1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
|
require 'delegate'
require 'optparse'
require 'fileutils'
require 'tempfile'
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.
def self.included(base)
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)
Time.now.utc.strftime("%Y%m%d%H%M%S")
end
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
private
# Ask the user interactively whether to force collision.
def force_file_collision?(destination, src, dst, file_options = {}, &block)
$stdout.print "overwrite #{destination}? (enter \"h\" for help) [Ynaqdh] "
case $stdin.gets.chomp
when /\Ad\z/i
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'
when /\Aa\z/i
$stdout.puts "forcing #{spec.name}"
options[:collision] = :force
when /\Aq\z/i
$stdout.puts "aborting #{spec.name}"
raise SystemExit
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'
end
rescue
retry
end
def diff_cmd
ENV['RAILS_DIFF'] || 'diff -u'
end
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)
# Initialize some check varibles
last_class = Object
current_class = nil
name = nil
class_names.flatten.each do |class_name|
# Convert to string to allow symbol arguments.
class_name = class_name.to_s
# Skip empty strings.
class_name.strip.empty? ? next : current_class = class_name
# Split the class from its module nesting.
nesting = class_name.split('::')
name = nesting.pop
# Extract the last Module in the nesting.
last = nesting.inject(last_class) { |last, nest|
break unless last_class.const_defined?(nest)
last_class = last_class.const_get(nest)
}
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)
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'
# :collision sets the collision option only for the destination file:
# 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)
destination_exists = File.exist?(destination)
# If source and destination are identical then we're done.
if destination_exists and identical?(source, destination, &block)
return logger.identical(relative_destination)
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
when :ask then force_file_collision?(relative_destination, source, destination, file_options, &block)
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.
File.open(destination, 'wb') do |dest|
dest.write render_file(source, file_options, &block)
end
# Optionally change permissions.
if file_options[:chmod]
FileUtils.chmod(file_options[:chmod], destination)
end
# Optionally add file to subversion or git
system("svn add #{destination}") if options[:svn]
system("git add -v #{relative_destination}") if options[:git]
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.
# Looks up and evaluates a template by name and writes the result.
#
# 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.
# Always skips directories which exist.
def directory(relative_path)
path = destination_path(relative_path)
if File.exist?(path)
logger.exists relative_path
else
logger.create relative_path
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
# 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
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
private
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
# Raise a usage error with an informative WordNet suggestion.
# Thanks to Florian Gross (flgr).
def raise_class_collision(class_name)
message = <<end_message
The name '#{class_name}' is either already used in your application or reserved by Ruby on Rails.
Please choose an alternative and run this generator again.
end_message
if suggest = find_synonyms(class_name)
if suggest.any?
message << "\n Suggestions: \n\n"
message << suggest.join("\n")
end
end
raise UsageError, message
end
SYNONYM_LOOKUP_URI = "http://wordnet.princeton.edu/perl/webwn?s=%s"
# 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|
# Grab words linked to dictionary entries as possible synonyms
data = stream.read.gsub(" ", " ").scan(/<a href="webwn.*?">([\w ]*?)<\/a>/s).uniq
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)
if File.exist?(destination)
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}")
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
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)
if File.exist?(path)
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
# I don't think git needs to remove directories?..
# or maybe they have special consideration...
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
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
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
def migration_template(relative_source, relative_destination, options = {})
migration_directory relative_destination
logger.migration_template file_name
end
def route_resources(*resources)
resource_list = resources.map { |r| r.to_sym.inspect }.join(', ')
logger.route "map.resources #{resource_list}"
end
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
logger.refreshing "#{template_options[:insert].gsub(/\.erb/,'')} inside #{relative_destination}"
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
|