aboutsummaryrefslogtreecommitdiffstats
path: root/railties/lib/rails_generator/commands.rb
blob: 3e02e3cc41d42702d8b4afac061d474b28dabe29 (plain) (blame)
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
require 'delegate'
require 'optparse'
require 'fileutils'
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.append_features(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 current_migration_number
            Dir.glob("#{@migration_directory}/[0-9]*.rb").inject(0) do |max, file_path|
              n = File.basename(file_path).split('_', 2).first.to_i
              if n > max then n else max end
            end
          end

          def next_migration_number
            current_migration_number + 1
          end

          def next_migration_string(padding = 3)
            "%.#{padding}d" % next_migration_number
          end

        private
          # Ask the user interactively whether to force collision.
          def force_file_collision?(destination)
            $stdout.print "overwrite #{destination}? [Ynaq] "
            case $stdin.gets
              when /a/i
                $stdout.puts "forcing #{spec.name}"
                options[:collision] = :force
              when /q/i
                $stdout.puts "aborting #{spec.name}"
                raise SystemExit
              when /n/i then :skip
              else :force
            end
          rescue
            retry
          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)
          class_names.flatten.each do |class_name|
            # Convert to string to allow symbol arguments.
            class_name = class_name.to_s

            # Skip empty strings.
            next if class_name.strip.empty?

            # Split the class from its module nesting.
            nesting = class_name.split('::')
            name = nesting.pop

            # Extract the last Module in the nesting.
            last = nesting.inject(Object) { |last, nest|
              break unless last.const_defined?(nest)
              last.const_get(nest)
            }

            # If the last Module exists, check whether the given
            # class exists and raise a collision if so.
            if last and last.const_defined?(name.camelize)
              raise_class_collision(class_name)
            end
          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.exists?(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)
              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 |df|
            File.open(source, 'rb') do |sf|
              if block_given?
                df.write(yield(sf))
              else
                if file_options[:shebang]
                  df.puts("#!#{file_options[:shebang]}")
                  if line = sf.gets
                    df.puts(line) if line !~ /^#!/
                  end
                end
                df.write(sf.read)
              end
            end
          end

          # Optionally change permissions.
          if file_options[:chmod]
            FileUtils.chmod(file_options[:chmod], destination)
          end
          
          # Optionally add file to subversion
          system("svn add #{destination}") if options[:svn]
        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 evalutes 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 directories which exist.
        def directory(relative_path)
          path = destination_path(relative_path)
          if File.exists?(path)
            logger.exists relative_path
          else
            logger.create relative_path
            FileUtils.mkdir_p(path) unless options[:pretend]
            
            # Optionally add file to subversion
            system("svn add #{path}") if options[:svn]
          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

        private
          # 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 reserved by Ruby on Rails.
  Please choose an alternative and run this generator again.
end_message
            if suggest = find_synonyms(class_name)
              message << "\n  Suggestions:  \n\n"
              message << suggest.join("\n")
            end
            raise UsageError, message
          end

          SYNONYM_LOOKUP_URI = "http://wordnet.princeton.edu/cgi-bin/webwn2.0?stage=2&word=%s&posnumber=1&searchtypenumber=2&senses=&showglosses=1"

          # 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|
                data = stream.read.gsub("&nbsp;", " ").gsub("<BR>", "")
                data.scan(/^Sense \d+\n.+?\n\n/m)
              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.exists?(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  
              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.exists?(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
                  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
          raise "There is no migration named #{file_name}" unless migration_exists?(file_name)
          existing_migrations(file_name).each do |file_path|
            file(relative_source, file_path, template_options)
          end
        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
      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(/\.rhtml/,'')} 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