From daee6fd92ac16878f6806c3382a9e74592aa9656 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 7 Feb 2005 13:14:05 +0000 Subject: Added new generator framework that informs about its doings on generation and enables updating and destruction of generated artifacts. See the new script/destroy and script/update for more details #487 [bitsweat] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@518 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- railties/lib/rails_generator.rb | 234 ++---------- railties/lib/rails_generator/base.rb | 195 ++++++++++ railties/lib/rails_generator/commands.rb | 409 +++++++++++++++++++++ .../generators/applications/app/USAGE | 16 + .../generators/applications/app/app_generator.rb | 118 ++++++ .../generators/components/controller/USAGE | 30 ++ .../components/controller/controller_generator.rb | 34 ++ .../components/controller/templates/controller.rb | 10 + .../controller/templates/functional_test.rb | 17 + .../components/controller/templates/helper.rb | 2 + .../components/controller/templates/view.rhtml | 2 + .../generators/components/mailer/USAGE | 19 + .../components/mailer/mailer_generator.rb | 26 ++ .../components/mailer/templates/fixture.rhtml | 3 + .../components/mailer/templates/mailer.rb | 13 + .../components/mailer/templates/unit_test.rb | 29 ++ .../components/mailer/templates/view.rhtml | 3 + .../generators/components/model/USAGE | 17 + .../generators/components/model/model_generator.rb | 13 + .../components/model/templates/fixtures.yml | 10 + .../generators/components/model/templates/model.rb | 2 + .../components/model/templates/unit_test.rb | 14 + .../generators/components/scaffold/USAGE | 32 ++ .../components/scaffold/scaffold_generator.rb | 161 ++++++++ .../components/scaffold/templates/controller.rb | 55 +++ .../components/scaffold/templates/form.rhtml | 5 + .../scaffold/templates/functional_test.rb | 80 ++++ .../components/scaffold/templates/layout.rhtml | 11 + .../components/scaffold/templates/style.css | 53 +++ .../components/scaffold/templates/view_edit.rhtml | 7 + .../components/scaffold/templates/view_list.rhtml | 24 ++ .../components/scaffold/templates/view_new.rhtml | 6 + .../components/scaffold/templates/view_show.rhtml | 8 + railties/lib/rails_generator/lookup.rb | 204 ++++++++++ railties/lib/rails_generator/manifest.rb | 53 +++ railties/lib/rails_generator/options.rb | 135 +++++++ railties/lib/rails_generator/scripts.rb | 81 ++++ railties/lib/rails_generator/scripts/destroy.rb | 11 + railties/lib/rails_generator/scripts/generate.rb | 11 + railties/lib/rails_generator/scripts/update.rb | 15 + railties/lib/rails_generator/simple_logger.rb | 46 +++ railties/lib/rails_generator/spec.rb | 44 +++ 42 files changed, 2057 insertions(+), 201 deletions(-) create mode 100644 railties/lib/rails_generator/base.rb create mode 100644 railties/lib/rails_generator/commands.rb create mode 100644 railties/lib/rails_generator/generators/applications/app/USAGE create mode 100644 railties/lib/rails_generator/generators/applications/app/app_generator.rb create mode 100644 railties/lib/rails_generator/generators/components/controller/USAGE create mode 100644 railties/lib/rails_generator/generators/components/controller/controller_generator.rb create mode 100644 railties/lib/rails_generator/generators/components/controller/templates/controller.rb create mode 100644 railties/lib/rails_generator/generators/components/controller/templates/functional_test.rb create mode 100644 railties/lib/rails_generator/generators/components/controller/templates/helper.rb create mode 100644 railties/lib/rails_generator/generators/components/controller/templates/view.rhtml create mode 100644 railties/lib/rails_generator/generators/components/mailer/USAGE create mode 100644 railties/lib/rails_generator/generators/components/mailer/mailer_generator.rb create mode 100644 railties/lib/rails_generator/generators/components/mailer/templates/fixture.rhtml create mode 100644 railties/lib/rails_generator/generators/components/mailer/templates/mailer.rb create mode 100644 railties/lib/rails_generator/generators/components/mailer/templates/unit_test.rb create mode 100644 railties/lib/rails_generator/generators/components/mailer/templates/view.rhtml create mode 100644 railties/lib/rails_generator/generators/components/model/USAGE create mode 100644 railties/lib/rails_generator/generators/components/model/model_generator.rb create mode 100644 railties/lib/rails_generator/generators/components/model/templates/fixtures.yml create mode 100644 railties/lib/rails_generator/generators/components/model/templates/model.rb create mode 100644 railties/lib/rails_generator/generators/components/model/templates/unit_test.rb create mode 100644 railties/lib/rails_generator/generators/components/scaffold/USAGE create mode 100644 railties/lib/rails_generator/generators/components/scaffold/scaffold_generator.rb create mode 100644 railties/lib/rails_generator/generators/components/scaffold/templates/controller.rb create mode 100644 railties/lib/rails_generator/generators/components/scaffold/templates/form.rhtml create mode 100644 railties/lib/rails_generator/generators/components/scaffold/templates/functional_test.rb create mode 100644 railties/lib/rails_generator/generators/components/scaffold/templates/layout.rhtml create mode 100644 railties/lib/rails_generator/generators/components/scaffold/templates/style.css create mode 100644 railties/lib/rails_generator/generators/components/scaffold/templates/view_edit.rhtml create mode 100644 railties/lib/rails_generator/generators/components/scaffold/templates/view_list.rhtml create mode 100644 railties/lib/rails_generator/generators/components/scaffold/templates/view_new.rhtml create mode 100644 railties/lib/rails_generator/generators/components/scaffold/templates/view_show.rhtml create mode 100644 railties/lib/rails_generator/lookup.rb create mode 100644 railties/lib/rails_generator/manifest.rb create mode 100644 railties/lib/rails_generator/options.rb create mode 100644 railties/lib/rails_generator/scripts.rb create mode 100644 railties/lib/rails_generator/scripts/destroy.rb create mode 100644 railties/lib/rails_generator/scripts/generate.rb create mode 100644 railties/lib/rails_generator/scripts/update.rb create mode 100644 railties/lib/rails_generator/simple_logger.rb create mode 100644 railties/lib/rails_generator/spec.rb (limited to 'railties/lib') diff --git a/railties/lib/rails_generator.rb b/railties/lib/rails_generator.rb index 83a404fc0a..0875a22dee 100644 --- a/railties/lib/rails_generator.rb +++ b/railties/lib/rails_generator.rb @@ -1,201 +1,33 @@ -require 'fileutils' - -module Rails - module Generator - class GeneratorError < StandardError; end - class UsageError < GeneratorError; end - - CONTRIB_ROOT = "#{RAILS_ROOT}/script/generators" - BUILTIN_ROOT = "#{File.dirname(__FILE__)}/../generators" - DEFAULT_SEARCH_PATHS = [CONTRIB_ROOT, BUILTIN_ROOT] - - class << self - def instance(name, args = [], search_paths = DEFAULT_SEARCH_PATHS) - # RAILS_ROOT constant must be set. - unless Object.const_get(:RAILS_ROOT) - raise GeneratorError, "RAILS_ROOT must be set. Did you require 'config/environment'?" - end - - # Force canonical name. - name = Inflector.underscore(name.downcase) - - # Search for filesystem path to requested generator. - unless path = find_generator_path(name, search_paths) - raise GeneratorError, "#{name} generator not found." - end - - # Check for templates directory. - template_root = "#{path}/templates" - unless File.directory?(template_root) - raise GeneratorError, "missing template directory #{template_root}" - end - - # Require class file according to naming convention. - require "#{path}/#{name}_generator.rb" - - # Find class according to naming convention. Allow Nesting::In::Modules. - class_name = Inflector.classify("#{name}_generator") - unless klass = find_generator_class(name) - raise GeneratorError, "no #{class_name} class defined in #{path}/#{name}_generator.rb" - end - - # Instantiate and return generator. - klass.new(template_root, RAILS_ROOT, search_paths, args) - end - - - def builtin_generators - generators([BUILTIN_ROOT]) - end - - def contrib_generators - generators([CONTRIB_ROOT]) - end - - def generators(search_paths) - generator_paths(search_paths).keys.uniq.sort - end - - # Find all generator paths. - def generator_paths(search_paths) - @paths ||= {} - unless @paths[search_paths] - paths = Hash.new { |h,k| h[k] = [] } - search_paths.each do |path| - Dir["#{path}/[a-z]*"].each do |dir| - paths[File.basename(dir)] << dir if File.directory?(dir) - end - end - @paths[search_paths] = paths - end - @paths[search_paths] - end - - def find_generator_path(name, search_paths) - generator_paths(search_paths)[name].first - end - - # Find all generator classes. - def generator_classes - classes = Hash.new { |h,k| h[k] = [] } - class_re = /([^:]+)Generator$/ - ObjectSpace.each_object(Class) do |object| - if md = class_re.match(object.name) and object < Rails::Generator::Base - classes[Inflector.underscore(md.captures.first)] << object - end - end - classes - end - - def find_generator_class(name) - generator_classes[name].first - end - end - - - # Talk about generators. - class Base - attr_reader :template_root, :destination_root, :args, :options, - :class_name, :singular_name, :plural_name - - alias_method :file_name, :singular_name - alias_method :table_name, :plural_name - - def self.generator_name - Inflector.underscore(name.gsub('Generator', '')) - end - - def initialize(template_root, destination_root, search_paths, args) - @template_root, @destination_root = template_root, destination_root - usage if args.empty? - @search_paths, @original_args = search_paths, args.dup - @class_name, @singular_name, @plural_name = inflect_names(args.shift) - @options = extract_options!(args) - @args = args - end - - # Checks whether the class name that was assigned to this generator - # would cause a collision with a Class, Module or other constant - # that is already used up by Ruby or RubyOnRails. - def collision_with_builtin? - builtin = Object.const_get(full_class_name) rescue nil - type = case builtin - when Class: "Class" - when Module: "Module" - else "Constant" - end - - if builtin then - "Sorry, you can't have a #{self.class.generator_name} named " + - "'#{full_class_name}' because Ruby or Rails already has a #{type} with that name.\n" + - "Please rerun the generator with a different name." - end - end - - # Returns the complete name that the resulting Class would have. - # Used in collision_with_builtin(). The default guess is that it is - # the same as class_name. Override this in your generator in case - # it is wrong. - def full_class_name - class_name - end - - protected - # Look up another generator with the same arguments. - def generator(name) - Rails::Generator.instance(name, @original_args, @search_paths) - end - - # Generate a file for a Rails application using an ERuby template. - # Looks up and evalutes a template by name and writes the result - # to a file relative to +destination_root+. The template - # is evaluated in the context of the optional eval_binding argument. - # - # 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. - def template(template_name, destination_path, eval_binding = nil) - # Determine full paths for source and destination files. - template_path = find_template_path(template_name) - destination_path = File.join(destination_root, destination_path) - - # Create destination directories. - FileUtils.mkdir_p(File.dirname(destination_path)) - - # Render template and write result. - eval_binding ||= binding - contents = ERB.new(File.read(template_path), nil, '-').result(eval_binding) - File.open(destination_path, 'w') { |file| file.write(contents) } - end - - def usage - raise UsageError.new, File.read(usage_path) - end - - private - def find_template_path(template_name) - name, path = template_name.split('/', 2) - if path.nil? - File.join(template_root, name) - elsif generator_path = Rails::Generator.find_generator_path(name, @search_paths) - File.join(generator_path, 'templates', path) - end - end - - def inflect_names(name) - camel = Inflector.camelize(Inflector.underscore(name)) - under = Inflector.underscore(camel) - plural = Inflector.pluralize(under) - [camel, under, plural] - end - - def extract_options!(args) - if args.last.is_a?(Hash) then args.pop else {} end - end - - def usage_path - "#{template_root}/../USAGE" - end - end - end -end +#-- +# Copyright (c) 2004 Jeremy Kemper +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +$:.unshift(File.dirname(__FILE__)) + +require 'support/core_ext' + +require 'rails_generator/base' +require 'rails_generator/lookup' +require 'rails_generator/commands' + +Rails::Generator::Base.send(:include, Rails::Generator::Lookup) +Rails::Generator::Base.send(:include, Rails::Generator::Commands) diff --git a/railties/lib/rails_generator/base.rb b/railties/lib/rails_generator/base.rb new file mode 100644 index 0000000000..065ce63966 --- /dev/null +++ b/railties/lib/rails_generator/base.rb @@ -0,0 +1,195 @@ +require File.dirname(__FILE__) + '/../support/class_attribute_accessors' +require File.dirname(__FILE__) + '/../support/inflector' +require File.dirname(__FILE__) + '/options' +require File.dirname(__FILE__) + '/manifest' +require File.dirname(__FILE__) + '/spec' + +# Rails::Generator is a code generation platform tailored for the Rails +# web application framework. Generators are easily invoked within Rails +# applications to add and remove components such as models and controllers. +# New generators are easy to create and may be distributed as RubyGems or +# tarballs for inclusion system-wide, per-user, or per-application. +# +# Generators may subclass other generators to provide variations that +# require little or no new logic but replace the template files. +# The postback generator is an example: it subclasses the scaffold +# generator and just replaces the code templates with its own. +# +# Now go forth and multiply^Wgenerate. +module Rails + module Generator + class GeneratorError < StandardError; end + class UsageError < GeneratorError; end + + + # The base code generator is bare-bones. It sets up the source and + # destination paths and tells the logger whether to keep its trap shut. + # You're probably looking for NamedBase, a subclass meant for generating + # "named" components such as models, controllers, and mailers. + # + # Generators create a manifest of the actions they perform then hand + # the manifest to a command which replay the actions to do the heavy + # lifting. Create, destroy, and list commands are included. Since a + # single manifest may be used by any command, creating new generators is + # as simple as writing some code templates and declaring what you'd like + # to do with them. + # + # The manifest method must be implemented by subclasses, returning a + # Rails::Generator::Manifest. The record method is provided as a + # convenience for manifest creation. Example: + # class EliteGenerator < Rails::Generator::Base + # def manifest + # record do |m| + # m.do(some) + # m.things(in) { here } + # end + # end + # end + class Base + include Options + + # Declare default options for the generator. These options + # are inherited to subclasses. + default_options :collision => :ask, :quiet => false + + # A logger instance available everywhere in the generator. + cattr_accessor :logger + + # Every generator that is dynamically looked up is tagged with a + # Spec describing where it was found. + class_inheritable_accessor :spec + + attr_reader :source_root, :destination_root, :args + + def initialize(runtime_args, runtime_options = {}) + @args = runtime_args + parse!(@args, runtime_options) + + # Derive source and destination paths. + @source_root = options[:source] || File.join(spec.path, 'templates') + if options[:destination] + @destination_root = options[:destination] + elsif Object.const_defined?(:RAILS_ROOT) + @destination_root = Object.const_get(:RAILS_ROOT) + end + + # Silence the logger if requested. + logger.quiet = options[:quiet] + + # Raise usage error if help is requested. + usage if options[:help] + end + + # Generators must provide a manifest. Use the record method to create + # a new manifest and record your generator's actions. + def manifest + raise NotImplementedError, "No manifest for '#{spec.name}' generator." + end + + # Return the full path from the source root for the given path. + # Example for source_root = '/source': + # source_path('some/path.rb') == '/source/some/path.rb' + # + # The given path may include a colon ':' character to indicate that + # the file belongs to another generator. This notation allows any + # generator to borrow files from another. Example: + # source_path('model:fixture.yml') = '/model/source/path/fixture.yml' + def source_path(relative_source) + # Check whether we're referring to another generator's file. + name, path = relative_source.split(':', 2) + + # If not, return the full path to our source file. + if path.nil? + File.join(source_root, name) + + # Otherwise, ask our referral for the file. + else + # FIXME: this is broken, though almost always true. Others' + # source_root are not necessarily the templates dir. + File.join(self.class.lookup(name).path, 'templates', path) + end + end + + # Return the full path from the destination root for the given path. + # Example for destination_root = '/dest': + # destination_path('some/path.rb') == '/dest/some/path.rb' + def destination_path(relative_destination) + File.join(destination_root, relative_destination) + end + + protected + # Convenience method for generator subclasses to record a manifest. + def record + Rails::Generator::Manifest.new(self) { |m| yield m } + end + + # Override with your own usage banner. + def banner + "Usage: #{$0} #{spec.name} [options]" + end + + # Read USAGE from file in generator base path. + def usage_message + File.read(File.join(spec.path, 'USAGE')) rescue '' + end + end + + + # The base generator for named components: models, controllers, mailers, + # etc. The target name is taken as the first argument and inflected to + # singular, plural, class, file, and table forms for your convenience. + # The remaining arguments are aliased to actions for controller and + # mailer convenience. + # + # If no name is provided, the generator raises a usage error with content + # optionally read from the USAGE file in the generator's base path. + # + # See Rails::Generator::Base for a discussion of Manifests and Commands. + class NamedBase < Base + attr_reader :name, :class_name, :singular_name, :plural_name + attr_reader :class_path, :class_nesting + alias_method :file_name, :singular_name + alias_method :table_name, :plural_name + alias_method :actions, :args + + def initialize(runtime_args, runtime_options = {}) + super + + # Name argument is required. + usage if runtime_args.empty? + + @args = runtime_args.dup + base_name = @args.shift + assign_names!(base_name) + end + + protected + # Override with your own usage banner. + def banner + "Usage: #{$0} #{spec.name} #{spec.name.camelize}Name [options]" + end + + private + def assign_names!(name) + @name = name + base_name, @class_path, @class_nesting = extract_modules(@name) + @class_name, @singular_name, @plural_name = inflect_names(base_name) + end + + def extract_modules(name) + modules = name.split('/') + name = modules.pop + path = modules.map { |m| m.underscore } + nesting = modules.map { |m| m.camelize }.join('::') + [name, path, nesting] + end + + def inflect_names(name) + camel = name.camelize + under = camel.underscore + plural = under.pluralize + [camel, under, plural] + end + end + end +end diff --git a/railties/lib/rails_generator/commands.rb b/railties/lib/rails_generator/commands.rb new file mode 100644 index 0000000000..ff68afce3e --- /dev/null +++ b/railties/lib/rails_generator/commands.rb @@ -0,0 +1,409 @@ +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 + + 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) + "\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) + 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 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' + # + # 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 = {}) + # Determine full paths for source and destination files. + source = source_path(relative_source) + destination = destination_path(relative_destination) + + # Check for and resolve file collisions. + if File.exists?(destination) + + # Make a choice whether to overwrite the file. :force and + # :skip already have their mind made up, but give :ask a shot. + choice = case 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, 'w') do |df| + File.open(source) do |sf| + if block_given? + df.write(yield(sf)) + else + line = sf.gets + if file_options[:shebang] + df.puts("#!#{file_options[:shebang]}") + df.puts(line) if line !~ /^#!/ + else + df.puts(line) + end + df.write(sf.read) + end + end + end + + # Optionally change permissions. + if file_options[:chmod] + FileUtils.chmod(file_options[:chmod], destination) + end + 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] + 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 + + private + # Raise a usage error with an informative WordNet suggestion. + # Thanks to Florian Gross (flgr). + def raise_class_collision(class_name) + message = <", "") + 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, options = {}) + destination = destination_path(relative_destination) + if File.exists?(destination) + logger.rm relative_destination + FileUtils.rm(destination) unless options[:pretend] + 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 + FileUtils.rmdir(path) unless options[:pretend] + else + logger.notempty partial + end + else + logger.missing partial + end + parts.pop + end + end + + def complex_template(*args) + # nothing should be done here + 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 + 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 diff --git a/railties/lib/rails_generator/generators/applications/app/USAGE b/railties/lib/rails_generator/generators/applications/app/USAGE new file mode 100644 index 0000000000..3bb55113fa --- /dev/null +++ b/railties/lib/rails_generator/generators/applications/app/USAGE @@ -0,0 +1,16 @@ +Description: + The 'rails' command creates a new Rails application with a default + directory structure and configuration at the path you specify. + +Example: + rails ~/Code/Ruby/weblog + + This generates a skeletal Rails installation in ~/Code/Ruby/weblog. + See the README in the newly created application to get going. + +WARNING: + Only specify --without-gems if you did not use gems to install Rails. + Your application will expect to find activerecord, actionpack, and + actionmailer directories in the vendor directory. A popular way to track + the bleeding edge of Rails development is to checkout from source control + directly to the vendor directory. See http://dev.rubyonrails.com diff --git a/railties/lib/rails_generator/generators/applications/app/app_generator.rb b/railties/lib/rails_generator/generators/applications/app/app_generator.rb new file mode 100644 index 0000000000..0beb11b237 --- /dev/null +++ b/railties/lib/rails_generator/generators/applications/app/app_generator.rb @@ -0,0 +1,118 @@ +class AppGenerator < Rails::Generator::Base + DEFAULT_SHEBANG = File.join(Config::CONFIG['bindir'], + Config::CONFIG['ruby_install_name']) + + default_options :gem => true, :shebang => DEFAULT_SHEBANG + mandatory_options :source => "#{File.dirname(__FILE__)}/../.." + + def initialize(runtime_args, runtime_options = {}) + super + usage if args.empty? + @destination_root = args.shift + puts "eek! #{destination_root.inspect}" + end + + def manifest + script_options = { :chmod => 0755, :shebang => options[:shebang] } + + record do |m| + # Root directory and all subdirectories. + m.directory '' + BASEDIRS.each { |path| m.directory path } + + # Root + m.file "fresh_rakefile", "Rakefile" + m.file "README", "README" + m.file "CHANGELOG", "CHANGELOG" + + # Application + m.template "helpers/application.rb", "app/controllers/application.rb" + m.template "helpers/application_helper.rb", "app/helpers/application_helper.rb" + m.template "helpers/test_helper.rb", "test/test_helper.rb" + + # database.yml and .htaccess + m.template "configs/database.yml", "config/database.yml" + m.template "configs/apache.conf", "public/.htaccess" + + # Environments + if options[:gem] + m.file "environments/shared_for_gem.rb", "config/environment.rb" + else + m.file "environments/shared.rb", "config/environment.rb" + end + m.file "environments/production.rb", "config/environments/production.rb" + m.file "environments/development.rb", "config/environments/development.rb" + m.file "environments/test.rb", "config/environments/test.rb" + + # Scripts + %w(console destroy generate server).each do |file| + m.file "bin/#{file}", "script/#{file}", script_options + end + if options[:gem] + m.file "bin/breakpointer_for_gem", "script/breakpointer", script_options + else + m.file "bin/breakpointer", "script/breakpointer", script_options + end + + # Dispatches + m.file "dispatches/dispatch.rb", "public/dispatch.rb", script_options + m.file "dispatches/dispatch.rb", "public/dispatch.cgi", script_options + m.file "dispatches/dispatch.fcgi", "public/dispatch.fcgi", script_options + + # HTML files + %w(404 500 index).each do |file| + m.template "html/#{file}.html", "public/#{file}.html" + end + + # Docs + m.template "doc/index.html", "public/_doc/index.html" + m.file "doc/README_FOR_APP", "doc/README_FOR_APP" + + # Logs + %w(apache production development test).each { |file| + m.file "configs/empty.log", "log/#{file}.log", :chmod => 0666 + } + end + end + + protected + def banner + "Usage: #{$0} /path/to/your/app [options]" + end + + def add_options!(opt) + opt.separator '' + opt.separator 'Options:' + opt.on("--ruby [#{DEFAULT_SHEBANG}]", + "Path to the Ruby binary of your choice.") { |options[:shebang]| } + opt.on("--without-gems", + "Don't use the Rails gems for your app.", + "WARNING: see note below.") { |options[:gem]| } + end + + + # Installation skeleton. Intermediate directories are automatically + # created so don't sweat their absence here. + BASEDIRS = %w( + app/controllers + app/helpers + app/models + app/views/layouts + config/environments + db + doc + lib + log + public/_doc + public/images + public/javascripts + public/stylesheets + script + test/fixtures + test/functional + test/mocks/development + test/mocks/testing + test/unit + vendor + ) +end diff --git a/railties/lib/rails_generator/generators/components/controller/USAGE b/railties/lib/rails_generator/generators/components/controller/USAGE new file mode 100644 index 0000000000..ec64209135 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/controller/USAGE @@ -0,0 +1,30 @@ +Description: + The controller generator creates stubs for a new controller and its views. + + The generator takes a controller name and a list of views as arguments. + The controller name may be given in CamelCase or under_score and should + not be suffixed with 'Controller'. To create a controller within a + module, specify the controller name as 'module/controller'. + + The generator creates a controller class in app/controllers with view + templates in app/views/controller_name, a helper class in app/helpers, + and a functional test suite in test/functional. + +Example: + ./script/generate controller CreditCard open debit credit close + + Credit card controller with URLs like /credit_card/debit. + Controller: app/controllers/credit_card_controller.rb + Views: app/views/credit_card/debit.rhtml [...] + Helper: app/helpers/credit_card_helper.rb + Test: test/functional/credit_card_controller_test.rb + +Modules Example: + ./script/generate controller 'admin/credit_card' suspend late_fee + + Credit card admin controller with URLs /admin/credit_card/suspend. + Controller: app/controllers/admin/credit_card_controller.rb + Views: app/views/admin/credit_card/debit.rhtml [...] + Helper: app/helpers/admin/credit_card_helper.rb + Test: test/functional/admin/credit_card_controller_test.rb + diff --git a/railties/lib/rails_generator/generators/components/controller/controller_generator.rb b/railties/lib/rails_generator/generators/components/controller/controller_generator.rb new file mode 100644 index 0000000000..1f7e69d124 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/controller/controller_generator.rb @@ -0,0 +1,34 @@ +class ControllerGenerator < Rails::Generator::NamedBase + def manifest + record do |m| + # Check for class naming collisions. + m.class_collisions "#{class_name}Controller", "#{class_name}ControllerTest", "#{class_name}Helper" + + # Views directory even if there are no actions. + m.directory File.join('app/views', class_path, file_name) + + # Controller class, functional test, and helper class. + m.template 'controller.rb', + File.join('app/controllers', + class_path, + "#{file_name}_controller.rb") + + m.template 'functional_test.rb', + File.join('test/functional', + class_path, + "#{file_name}_controller_test.rb") + + m.template 'helper.rb', + File.join('app/helpers', + class_path, + "#{file_name}_helper.rb") + + # View template for each action. + actions.each do |action| + m.template 'view.rhtml', + File.join('app/views', class_path, file_name, "#{action}.rhtml"), + :assigns => { :action => action } + end + end + end +end diff --git a/railties/lib/rails_generator/generators/components/controller/templates/controller.rb b/railties/lib/rails_generator/generators/components/controller/templates/controller.rb new file mode 100644 index 0000000000..da71b5f057 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/controller/templates/controller.rb @@ -0,0 +1,10 @@ +class <%= class_name %>Controller < ApplicationController +<% if options[:scaffold] -%> + scaffold :<%= singular_name %> +<% end -%> +<% for action in actions -%> + + def <%= action %> + end +<% end -%> +end diff --git a/railties/lib/rails_generator/generators/components/controller/templates/functional_test.rb b/railties/lib/rails_generator/generators/components/controller/templates/functional_test.rb new file mode 100644 index 0000000000..c975cb3ce3 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/controller/templates/functional_test.rb @@ -0,0 +1,17 @@ +require File.dirname(__FILE__) + '/../test_helper' +require '<%= file_name %>_controller' + +# Re-raise errors caught by the controller. +class <%= class_name %>Controller; def rescue_action(e) raise e end; end + +class <%= class_name %>ControllerTest < Test::Unit::TestCase + def setup + @controller = <%= class_name %>Controller.new + @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new + end + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/railties/lib/rails_generator/generators/components/controller/templates/helper.rb b/railties/lib/rails_generator/generators/components/controller/templates/helper.rb new file mode 100644 index 0000000000..3fe2ecdc74 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/controller/templates/helper.rb @@ -0,0 +1,2 @@ +module <%= class_name %>Helper +end diff --git a/railties/lib/rails_generator/generators/components/controller/templates/view.rhtml b/railties/lib/rails_generator/generators/components/controller/templates/view.rhtml new file mode 100644 index 0000000000..7e7a7d53ce --- /dev/null +++ b/railties/lib/rails_generator/generators/components/controller/templates/view.rhtml @@ -0,0 +1,2 @@ +

<%= class_name %>#<%= action %>

+

Find me in app/views/<%= file_name %>/<%= action %>.rhtml

diff --git a/railties/lib/rails_generator/generators/components/mailer/USAGE b/railties/lib/rails_generator/generators/components/mailer/USAGE new file mode 100644 index 0000000000..50d3de19de --- /dev/null +++ b/railties/lib/rails_generator/generators/components/mailer/USAGE @@ -0,0 +1,19 @@ +Description: + The mailer generator creates stubs for a new mailer and its views. + + The generator takes a mailer name and a list of views as arguments. + The mailer name may be given in CamelCase or under_score and should + not be suffixed with 'Mailer'. + + The generator creates a mailer class in app/models with view templates + in app/views/mailer_name, and a test suite with fixtures in test/unit. + +Example: + ./script/generate mailer Notifications signup forgot_password invoice + + This will create a NotificationsMailer: + Mailer: app/models/notifications.rb + Views: app/views/notifications/signup.rhtml [...] + Test: test/unit/credit_card_controller_test.rb + Fixtures: test/fixtures/notifications/signup [...] + diff --git a/railties/lib/rails_generator/generators/components/mailer/mailer_generator.rb b/railties/lib/rails_generator/generators/components/mailer/mailer_generator.rb new file mode 100644 index 0000000000..81d4599f7f --- /dev/null +++ b/railties/lib/rails_generator/generators/components/mailer/mailer_generator.rb @@ -0,0 +1,26 @@ +class MailerGenerator < Rails::Generator::NamedBase + def manifest + record do |m| + # Check for class naming collisions. + m.class_collisions class_name, "#{class_name}Test" + + # Mailer class and unit test. + m.template "mailer.rb", "app/models/#{file_name}.rb" + m.template "unit_test.rb", "test/unit/#{file_name}_test.rb" + + # Views and fixtures directories. + m.directory "app/views/#{file_name}" + m.directory "test/fixtures/#{table_name}" + + # View template and fixture for each action. + actions.each do |action| + m.template "view.rhtml", + "app/views/#{file_name}/#{action}.rhtml", + :assigns => { :action => action } + m.template "fixture.rhtml", + "test/fixtures/#{table_name}/#{action}", + :assigns => { :action => action } + end + end + end +end diff --git a/railties/lib/rails_generator/generators/components/mailer/templates/fixture.rhtml b/railties/lib/rails_generator/generators/components/mailer/templates/fixture.rhtml new file mode 100644 index 0000000000..b481906829 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/mailer/templates/fixture.rhtml @@ -0,0 +1,3 @@ +<%= class_name %>#<%= action %> + +Find me in app/views/<%= file_name %>/<%= action %>.rhtml diff --git a/railties/lib/rails_generator/generators/components/mailer/templates/mailer.rb b/railties/lib/rails_generator/generators/components/mailer/templates/mailer.rb new file mode 100644 index 0000000000..81c19fa76d --- /dev/null +++ b/railties/lib/rails_generator/generators/components/mailer/templates/mailer.rb @@ -0,0 +1,13 @@ +class <%= class_name %> < ActionMailer::Base +<% for action in actions -%> + + def <%= action %>(sent_on = Time.now) + @subject = '<%= class_name %>#<%= action %>' + @body = {} + @recipients = '' + @from = '' + @sent_on = sent_on + @headers = {} + end +<% end -%> +end diff --git a/railties/lib/rails_generator/generators/components/mailer/templates/unit_test.rb b/railties/lib/rails_generator/generators/components/mailer/templates/unit_test.rb new file mode 100644 index 0000000000..70fd3afe37 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/mailer/templates/unit_test.rb @@ -0,0 +1,29 @@ +require File.dirname(__FILE__) + '/../test_helper' +require '<%= file_name %>' + +class <%= class_name %>Test < Test::Unit::TestCase + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures' + + def setup + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries = [] + + @expected = TMail::Mail.new + end + +<% for action in actions -%> + def test_<%= action %> + @expected.subject = '<%= class_name %>#<%= action %>' + @expected.body = read_fixture('<%= action %>') + @expected.date = Time.now + + assert_equal @expected.encoded, <%= class_name %>.create_<%= action %>(@expected.date).encoded + end + +<% end -%> + private + def read_fixture(action) + IO.readlines("#{FIXTURES_PATH}/<%= file_name %>/#{action}") + end +end diff --git a/railties/lib/rails_generator/generators/components/mailer/templates/view.rhtml b/railties/lib/rails_generator/generators/components/mailer/templates/view.rhtml new file mode 100644 index 0000000000..b481906829 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/mailer/templates/view.rhtml @@ -0,0 +1,3 @@ +<%= class_name %>#<%= action %> + +Find me in app/views/<%= file_name %>/<%= action %>.rhtml diff --git a/railties/lib/rails_generator/generators/components/model/USAGE b/railties/lib/rails_generator/generators/components/model/USAGE new file mode 100644 index 0000000000..f0669104fe --- /dev/null +++ b/railties/lib/rails_generator/generators/components/model/USAGE @@ -0,0 +1,17 @@ +Description: + The model generator creates stubs for a new model. + + The generator takes a model name as its argument. The model name may be + given in CamelCase or under_score and should not be suffixed with 'Model'. + + The generator creates a model class in app/models, a test suite in + test/unit, and test fixtures in test/fixtures/model_name.yml. + +Example: + ./script/generate model Account + + This will create an Account model: + Model: app/models/account.rb + Test: test/unit/account_test.rb + Fixtures: test/fixtures/accounts.yml + diff --git a/railties/lib/rails_generator/generators/components/model/model_generator.rb b/railties/lib/rails_generator/generators/components/model/model_generator.rb new file mode 100644 index 0000000000..32577d08a3 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/model/model_generator.rb @@ -0,0 +1,13 @@ +class ModelGenerator < Rails::Generator::NamedBase + def manifest + record do |m| + # Check for class naming collisions. + m.class_collisions class_name, "#{class_name}Test" + + # Model class, unit test, and fixtures. + m.template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb") + m.template 'unit_test.rb', File.join('test/unit', class_path, "#{file_name}_test.rb") + m.template 'fixtures.yml', File.join('test/fixtures', class_path, "#{table_name}.yml") + end + end +end diff --git a/railties/lib/rails_generator/generators/components/model/templates/fixtures.yml b/railties/lib/rails_generator/generators/components/model/templates/fixtures.yml new file mode 100644 index 0000000000..fc3185dc46 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/model/templates/fixtures.yml @@ -0,0 +1,10 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +# Set the $base_id variable in the setup method of your tests. +# It's used to ensure that ids don't clash in some databases. +<%% $base_id ||= 100000 %> + +first_<%= singular_name %>: + id: <%%= $base_id %> + +another_<%= singular_name %>: + id: <%%= $base_id + 1 %> diff --git a/railties/lib/rails_generator/generators/components/model/templates/model.rb b/railties/lib/rails_generator/generators/components/model/templates/model.rb new file mode 100644 index 0000000000..8d4c89e912 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/model/templates/model.rb @@ -0,0 +1,2 @@ +class <%= class_name %> < ActiveRecord::Base +end diff --git a/railties/lib/rails_generator/generators/components/model/templates/unit_test.rb b/railties/lib/rails_generator/generators/components/model/templates/unit_test.rb new file mode 100644 index 0000000000..db0fbf5d33 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/model/templates/unit_test.rb @@ -0,0 +1,14 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class <%= class_name %>Test < Test::Unit::TestCase + fixtures :<%= table_name %> + + def setup + $base_id = 1000001 + end + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/railties/lib/rails_generator/generators/components/scaffold/USAGE b/railties/lib/rails_generator/generators/components/scaffold/USAGE new file mode 100644 index 0000000000..d1f29ad63c --- /dev/null +++ b/railties/lib/rails_generator/generators/components/scaffold/USAGE @@ -0,0 +1,32 @@ +Description: + The scaffold generator creates a controller to interact with a model. + If the model does not exist, it creates the model as well. The generated + code is equivalent to the "scaffold :model" declaration, making it easy + to migrate when you wish to customize your controller and views. + + The generator takes a model name, an optional controller name, and a + list of views as arguments. Scaffolded actions and views are created + automatically. Any views left over generate empty stubs. + + The scaffolded actions and views are: + index, list, show, new, create, edit, update, destroy + + If a controller name is not given, the plural form of the model name + will be used. The model and controller names may be given in CamelCase + or under_score and should not be suffixed with 'Model' or 'Controller'. + Both model and controller names may be prefixed with a module like a + file path; see the Modules Example for usage. + +Example: + ./script/generate scaffold Account Bank debit credit + + This will generate an Account model and BankController with a full test + suite and a basic user interface. Now create the accounts table in your + database and browse to http://localhost/bank/ -- voila, you're on Rails! + +Modules Example: + ./script/generate controller 'admin/credit_card' suspend late_fee + + This will generate a CreditCard model and CreditCardController controller + in the admin module. + diff --git a/railties/lib/rails_generator/generators/components/scaffold/scaffold_generator.rb b/railties/lib/rails_generator/generators/components/scaffold/scaffold_generator.rb new file mode 100644 index 0000000000..4445995b46 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/scaffold/scaffold_generator.rb @@ -0,0 +1,161 @@ +class ScaffoldingSandbox + include ActionView::Helpers::ActiveRecordHelper + + attr_accessor :form_action, :singular_name, :suffix, :model_instance + + def sandbox_binding + binding + end +end + +class ActionView::Helpers::InstanceTag + def to_input_field_tag(field_type, options={}) + field_meth = "#{field_type}_field" + "<%= #{field_meth} '#{@object_name}', '#{@method_name}' #{options.empty? ? '' : ', '+options.inspect} %>" + end + + def to_text_area_tag(options = {}) + "<%= text_area '#{@object_name}', '#{@method_name}' #{options.empty? ? '' : ', '+ options.inspect} %>" + end + + def to_date_select_tag(options = {}) + "<%= date_select '#{@object_name}', '#{@method_name}' #{options.empty? ? '' : ', '+ options.inspect} %>" + end + + def to_datetime_select_tag(options = {}) + "<%= datetime_select '#{@object_name}', '#{@method_name}' #{options.empty? ? '' : ', '+ options.inspect} %>" + end +end + +class ScaffoldGenerator < Rails::Generator::NamedBase + attr_reader :controller_name, + :controller_class_path, + :controller_class_nesting, + :controller_class_name, + :controller_singular_name, + :controller_plural_name + alias_method :controller_file_name, :controller_singular_name + alias_method :controller_table_name, :controller_plural_name + + def initialize(runtime_args, runtime_options = {}) + super + @controller_name = args.shift || @name.pluralize + base_name, @controller_class_path, @controller_class_nesting = extract_modules(@controller_name) + @controller_class_name, @controller_singular_name, @controller_plural_name = inflect_names(base_name) + end + + def manifest + record do |m| + # Depend on model generator but skip if the model exists. + m.dependency 'model', [@name], :collision => :skip + + # Check for class naming collisions. + m.class_collisions "#{controller_class_name}Controller", "#{controller_class_name}ControllerTest", "#{controller_class_name}Helper" + + # Views directory. + m.directory File.join('app/views', controller_class_path, controller_file_name) + + # Controller class, functional test, helper, and views. + m.template 'controller.rb', + File.join('app/controllers', + controller_class_path, + "#{controller_file_name}_controller.rb") + + m.template 'functional_test.rb', + File.join('test/functional', + controller_class_path, + "#{controller_file_name}_controller_test.rb") + + m.template 'controller:helper.rb', + File.join('app/helpers', + controller_class_path, + "#{controller_file_name}_helper.rb") + + # Layout and stylesheet. + m.template 'layout.rhtml', "app/views/layouts/#{controller_file_name}.rhtml" + m.template 'style.css', 'public/stylesheets/scaffold.css' + + # Scaffolded views. + scaffold_views.each do |action| + m.template "view_#{action}.rhtml", + File.join('app/views', + controller_class_path, controller_file_name, + "#{action}.rhtml"), + :assigns => { :action => action } + end + + # Scaffolded forms. + scaffold_forms.each do |action| + m.complex_template "view_#{action}.rhtml", + File.join('app/views', + controller_class_path, + controller_file_name, + "#{action}.rhtml"), + :assigns => { :action => action }, + :insert => 'form.rhtml', + :sandbox => lambda { create_sandbox(action) }, + :begin_mark => 'form', + :end_mark => 'eoform', + :mark_id => singular_name + end + + # Unscaffolded views. + unscaffolded_actions.each do |action| + m.template "controller:view.rhtml", + File.join('app/views', + controller_class_path, controller_file_name, + "#{action}.rhtml"), + :assigns => { :action => action } + end + end + end + + protected + # Override with your own usage banner. + def banner + "Usage: #{$0} scaffold ModelName [ControllerName] [action, ...]" + end + + def scaffold_views + %w(list show) + end + + def scaffold_forms + %w(new edit) + end + + def scaffold_actions + scaffold_views + %w(index create update destroy) + end + + def unscaffolded_actions + args - scaffold_actions + end + + def suffix + "_#{singular_name}" if options[:suffix] + end + + def create_sandbox(action) + sandbox = ScaffoldingSandbox.new + action = if action == 'edit' then 'update' else 'create' end + sandbox.form_action = action + sandbox.singular_name = singular_name + begin + sandbox.model_instance = model_instance + sandbox.instance_variable_set("@#{singular_name}", sandbox.model_instance) + rescue ActiveRecord::StatementInvalid => e + logger.error "Before updating scaffolding from new DB schema, try creating a table for your model (#{class_name})" + raise SystemExit + end + sandbox.suffix = suffix + sandbox + end + + def model_instance + unless Object.const_defined?(class_name) + Object.const_set(class_name, Class.new(ActiveRecord::Base)) + end + Object.const_get(class_name).new + end +end diff --git a/railties/lib/rails_generator/generators/components/scaffold/templates/controller.rb b/railties/lib/rails_generator/generators/components/scaffold/templates/controller.rb new file mode 100644 index 0000000000..c409284cb1 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/scaffold/templates/controller.rb @@ -0,0 +1,55 @@ +class <%= controller_class_name %>Controller < ApplicationController +<% unless suffix -%> + + def index + list + render_action 'list' + end +<% end -%> + +<% for action in unscaffolded_actions -%> + def <%= action %><%= suffix %> + end + +<% end -%> + def list<%= suffix %> + @<%= plural_name %> = <%= class_name %>.find_all + end + + def show<%= suffix %> + @<%= singular_name %> = <%= class_name %>.find(@params['id']) + end + + def new<%= suffix %> + @<%= singular_name %> = <%= class_name %>.new + end + + def create<%= suffix %> + @<%= singular_name %> = <%= class_name %>.new(@params['<%= singular_name %>']) + if @<%= singular_name %>.save + flash['notice'] = '<%= class_name %> was successfully created.' + redirect_to :action => 'list<%= suffix %>' + else + render_action 'new<%= suffix %>' + end + end + + def edit<%= suffix %> + @<%= singular_name %> = <%= class_name %>.find(@params['id']) + end + + def update + @<%= singular_name %> = <%= class_name %>.find(@params['<%= singular_name %>']['id']) + if @<%= singular_name %>.update_attributes(@params['<%= singular_name %>']) + flash['notice'] = '<%= class_name %> was successfully updated.' + redirect_to :action => 'show<%= suffix %>', :id => @<%= singular_name %>.id + else + render_action 'edit<%= suffix %>' + end + end + + def destroy<%= suffix %> + <%= class_name %>.find(@params['id']).destroy + redirect_to :action => 'list<%= suffix %>' + end +end diff --git a/railties/lib/rails_generator/generators/components/scaffold/templates/form.rhtml b/railties/lib/rails_generator/generators/components/scaffold/templates/form.rhtml new file mode 100644 index 0000000000..d314c5f6b5 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/scaffold/templates/form.rhtml @@ -0,0 +1,5 @@ +<%%= start_form_tag :action => '<%= @form_action %><%= @suffix %>' %> +<%%= hidden_field '<%= @singular_name %>', 'id' %> +<%= all_input_tags(@model_instance, @singular_name, {}) %> + +<%%= end_form_tag %> diff --git a/railties/lib/rails_generator/generators/components/scaffold/templates/functional_test.rb b/railties/lib/rails_generator/generators/components/scaffold/templates/functional_test.rb new file mode 100644 index 0000000000..ea9c8e4e94 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/scaffold/templates/functional_test.rb @@ -0,0 +1,80 @@ +require File.dirname(__FILE__) + '/../test_helper' +require '<%= controller_file_name %>_controller' + +# Re-raise errors caught by the controller. +class <%= controller_class_name %>Controller; def rescue_action(e) raise e end; end + +class <%= controller_class_name %>ControllerTest < Test::Unit::TestCase + fixtures :<%= table_name %> + + def setup + $base_id = 1000001 + @controller = <%= controller_class_name %>Controller.new + @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new + end + +<% for action in unscaffolded_actions -%> + def test_<%= action %> + process :<%= action %> + assert_rendered_file '<%= action %>' + end + +<% end -%> +<% unless suffix -%> + def test_index + process :index + assert_rendered_file 'list' + end + +<% end -%> + def test_list<%= suffix %> + process :list<%= suffix %> + assert_rendered_file 'list<%= suffix %>' + assert_template_has '<%= plural_name %>' + end + + def test_show<%= suffix %> + process :show<%= suffix %>, 'id' => $base_id + assert_rendered_file 'show' + assert_template_has '<%= singular_name %>' + assert_valid_record '<%= singular_name %>' + end + + def test_new<%= suffix %> + process :new<%= suffix %> + assert_rendered_file 'new<%= suffix %>' + assert_template_has '<%= singular_name %>' + end + + def test_create + num_<%= plural_name %> = <%= class_name %>.find_all.size + + process :create<%= suffix %>, '<%= singular_name %>' => { } + assert_redirected_to :action => 'list<%= suffix %>' + + assert_equal num_<%= plural_name %> + 1, <%= class_name %>.find_all.size + end + + def test_edit<%= suffix %> + process :edit<%= suffix %>, 'id' => $base_id + assert_rendered_file 'edit<%= suffix %>' + assert_template_has '<%= singular_name %>' + assert_valid_record '<%= singular_name %>' + end + + def test_update<%= suffix %> + process :update<%= suffix %>, '<%= singular_name %>' => { 'id' => $base_id } + assert_redirected_to :action => 'show<%= suffix %>', :id => $base_id + end + + def test_destroy<%= suffix %> + assert_not_nil <%= class_name %>.find($base_id) + + process :destroy, 'id' => $base_id + assert_redirected_to :action => 'list<%= suffix %>' + + assert_raise(ActiveRecord::RecordNotFound) { + <%= singular_name %> = <%= class_name %>.find($base_id) + } + end +end diff --git a/railties/lib/rails_generator/generators/components/scaffold/templates/layout.rhtml b/railties/lib/rails_generator/generators/components/scaffold/templates/layout.rhtml new file mode 100644 index 0000000000..c4815bd0a3 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/scaffold/templates/layout.rhtml @@ -0,0 +1,11 @@ + + + <%= controller_class_name %>: <%%= controller.action_name %> + + + + +<%%= @content_for_layout %> + + + diff --git a/railties/lib/rails_generator/generators/components/scaffold/templates/style.css b/railties/lib/rails_generator/generators/components/scaffold/templates/style.css new file mode 100644 index 0000000000..95a3c4668c --- /dev/null +++ b/railties/lib/rails_generator/generators/components/scaffold/templates/style.css @@ -0,0 +1,53 @@ +body { background-color: #fff; color: #333; } + +body, p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +pre { + background-color: #eee; + padding: 10px; + font-size: 11px; +} + +a { color: #000; } +a:visited { color: #666; } +a:hover { color: #fff; background-color:#000; } + +.fieldWithErrors { + padding: 2px; + background-color: red; + display: table; +} + +#ErrorExplanation { + width: 400px; + border: 2px solid #red; + padding: 7px; + padding-bottom: 12px; + margin-bottom: 20px; + background-color: #f0f0f0; +} + +#ErrorExplanation h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 5px 15px; + font-size: 12px; + margin: -7px; + background-color: #c00; + color: #fff; +} + +#ErrorExplanation p { + color: #333; + margin-bottom: 0; + padding: 5px; +} + +#ErrorExplanation ul li { + font-size: 12px; + list-style: square; +} diff --git a/railties/lib/rails_generator/generators/components/scaffold/templates/view_edit.rhtml b/railties/lib/rails_generator/generators/components/scaffold/templates/view_edit.rhtml new file mode 100644 index 0000000000..4ad70f537a --- /dev/null +++ b/railties/lib/rails_generator/generators/components/scaffold/templates/view_edit.rhtml @@ -0,0 +1,7 @@ +

Editing <%= singular_name %>

+ +<%%= error_messages_for '<%= singular_name %>' %> +<%= template_for_inclusion %> + +<%%= link_to 'Show', :action => 'show<%= suffix %>', :id => @<%= singular_name %>.id %> | +<%%= link_to 'Back', :action => 'list<%= suffix %>' %> diff --git a/railties/lib/rails_generator/generators/components/scaffold/templates/view_list.rhtml b/railties/lib/rails_generator/generators/components/scaffold/templates/view_list.rhtml new file mode 100644 index 0000000000..068fd67472 --- /dev/null +++ b/railties/lib/rails_generator/generators/components/scaffold/templates/view_list.rhtml @@ -0,0 +1,24 @@ +

Listing <%= plural_name %>

+ + + +<%% for column in <%= class_name %>.content_columns %> + +<%% end %> + + +<%% for <%= singular_name %> in @<%= plural_name %> %> + + <%% for column in <%= class_name %>.content_columns %> + + <%% end %> + + + + +<%% end %> +
<%%= column.human_name %>
<%%=h <%= singular_name %>[column.name] %><%%= link_to 'Show', :action => 'show<%= suffix %>', :id => <%= singular_name %>.id %><%%= link_to 'Edit', :action => 'edit<%= suffix %>', :id => <%= singular_name %>.id %><%%= link_to 'Destroy', :action => 'destroy<%= suffix %>', :id => <%= singular_name %>.id %>
+ +
+ +<%%= link_to 'New <%= singular_name %>', :action => 'new<%= suffix %>' %> diff --git a/railties/lib/rails_generator/generators/components/scaffold/templates/view_new.rhtml b/railties/lib/rails_generator/generators/components/scaffold/templates/view_new.rhtml new file mode 100644 index 0000000000..fcf5a3c54b --- /dev/null +++ b/railties/lib/rails_generator/generators/components/scaffold/templates/view_new.rhtml @@ -0,0 +1,6 @@ +

New <%= @singular_name %>

+ +<%%= error_messages_for '<%= singular_name %>' %> +<%= template_for_inclusion %> + +<%%= link_to 'Back', :action => 'list<%= suffix %>' %> diff --git a/railties/lib/rails_generator/generators/components/scaffold/templates/view_show.rhtml b/railties/lib/rails_generator/generators/components/scaffold/templates/view_show.rhtml new file mode 100644 index 0000000000..ba8f3616dd --- /dev/null +++ b/railties/lib/rails_generator/generators/components/scaffold/templates/view_show.rhtml @@ -0,0 +1,8 @@ +<%% for column in <%= class_name %>.content_columns %> +

+ <%%= column.human_name %>: <%%= @<%= singular_name %>.send(column.name) %> +

+<%% end %> + +<%%= link_to 'Edit', :action => 'edit<%= suffix %>', :id => @<%= singular_name %>.id %> | +<%%= link_to 'Back', :action => 'list<%= suffix %>' %> diff --git a/railties/lib/rails_generator/lookup.rb b/railties/lib/rails_generator/lookup.rb new file mode 100644 index 0000000000..ba47fd79be --- /dev/null +++ b/railties/lib/rails_generator/lookup.rb @@ -0,0 +1,204 @@ +require File.dirname(__FILE__) + '/spec' + +class Object + class << self + # Lookup missing generators using const_missing. This allows any + # generator to reference another without having to know its location: + # RubyGems, ~/.rails/generators, and RAILS_ROOT/script/generators all + # cooperate to get the job done. The greatest use of const_missing + # autoloading is to easily subclass existing generators. Example: + # class HorsebackGenerator < PostbackGenerator + # We don't know whether the postback generator is built in, installed + # as a gem, or in the user's home directory, and we shouldn't have to. + unless respond_to?(:pre_generator_const_missing) + alias_method :pre_generator_const_missing, :const_missing + + def const_missing(class_id) + if md = /(.+)Generator$/.match(class_id.to_s) + name = md.captures.first.demodulize.underscore + Rails::Generator::Base.lookup(name).klass + else + pre_generator_const_missing(class_id) + end + end + end + end +end + +# User home directory lookup adapted from RubyGems. +def Dir.user_home + if ENV['HOME'] + ENV['HOME'] + elsif ENV['USERPROFILE'] + ENV['USERPROFILE'] + elsif ENV['HOMEDRIVE'] and ENV['HOMEPATH'] + "#{ENV['HOMEDRIVE']}:#{ENV['HOMEPATH']}" + else + File.expand_path '~' + end +end + + +module Rails + module Generator + + # Generator lookup is managed by a list of sources which return specs + # describing where to find and how to create generators. This module + # provides class methods for manipulating the source list and looking up + # generator specs, and an #instance wrapper for quickly instantiating + # generators by name. + # + # A spec is not a generator: it's a description of where to find + # the generator and how to create it. A source is anything that + # yields generators from #each. PathSource and GemSource are provided. + module Lookup + def self.append_features(base) + super + base.extend(ClassMethods) + base.use_component_sources! + end + + # Convenience method to instantiate another generator. + def instance(generator_name, args, runtime_options = {}) + self.class.instance(generator_name, args, runtime_options) + end + + module ClassMethods + # The list of sources where we look, in order, for generators. + def sources + read_inheritable_attribute(:sources) or use_component_sources! + end + + # Add a source to the end of the list. + def append_sources(*args) + sources.concat(args.flatten) + invalidate_cache! + end + + # Add a source to the beginning of the list. + def prepend_sources(*args) + write_inheritable_array(:sources, args.flatten + sources) + invalidate_cache! + end + + # Reset the source list. + def reset_sources + write_inheritable_attribute(:sources, []) + invalidate_cache! + end + + # Use application generators (app, ?). + def use_application_sources! + reset_sources + sources << PathSource.new(:builtin, "#{File.dirname(__FILE__)}/generators/applications") + end + + # Use component generators (model, controller, etc). + # 1. Rails application. If RAILS_ROOT is defined we know we're + # generating in the context of a Rails application, so search + # RAILS_ROOT/script/generators. + # 2. User home directory. Search ~/.rails/generators. + # 3. RubyGems. Search for gems named *_generator. + # 4. Builtins. Model, controller, mailer, scaffold. + def use_component_sources! + reset_sources + sources << PathSource.new(:app, "#{Object.const_get(:RAILS_ROOT)}/script/generators") if Object.const_defined?(:RAILS_ROOT) + sources << PathSource.new(:user, "#{Dir.user_home}/.rails/generators") + sources << GemSource.new if Object.const_defined?(:Gem) + sources << PathSource.new(:builtin, "#{File.dirname(__FILE__)}/generators/components") + end + + # Lookup knows how to find generators' Specs from a list of Sources. + # Searches the sources, in order, for the first matching name. + def lookup(generator_name) + @found ||= {} + generator_name = generator_name.to_s.downcase + @found[generator_name] ||= cache.find { |spec| + spec.name == generator_name + } or raise GeneratorError, "Couldn't find '#{generator_name}' generator" + end + + # Convenience method to lookup and instantiate a generator. + def instance(generator_name, args = [], runtime_options = {}) + lookup(generator_name).klass.new(args, full_options(runtime_options)) + end + + private + # Lookup and cache every generator from the source list. + def cache + @cache ||= sources.inject([]) { |cache, source| cache + source.map } + end + + # Clear the cache whenever the source list changes. + def invalidate_cache! + @cache = nil + end + end + end + + # Sources enumerate (yield from #each) generator specs which describe + # where to find and how to create generators. Enumerable is mixed in so, + # for example, source.collect will retrieve every generator. + # Sources may be assigned a label to distinguish them. + class Source + include Enumerable + + attr_reader :label + def initialize(label) + @label = label + end + + # The each method must be implemented in subclasses. + # The base implementation raises an error. + def each + raise NotImplementedError + end + + # Return a convenient sorted list of all generator names. + def names + map { |spec| spec.name }.sort + end + end + + + # PathSource looks for generators in a filesystem directory. + class PathSource < Source + attr_reader :path + + def initialize(label, path) + super label + @path = path + end + + # Yield each eligible subdirectory. + def each + Dir["#{path}/[a-z]*"].each do |dir| + if File.directory?(dir) + yield Spec.new(File.basename(dir), dir, label) + end + end + end + end + + + # GemSource hits the mines to quarry for generators. The latest versions + # of gems named *_generator are selected. + class GemSource < Source + def initialize + super :RubyGems + end + + # Yield latest versions of generator gems. + def each + Gem::cache.search(/_generator$/).inject({}) { |latest, gem| + hem = latest[gem.name] + latest[gem.name] = gem if hem.nil? or gem.version > hem.version + latest + }.values.each { |gem| + yield Spec.new(gem.name.sub(/_generator$/, ''), gem.full_gem_path, label) + } + end + end + + end +end diff --git a/railties/lib/rails_generator/manifest.rb b/railties/lib/rails_generator/manifest.rb new file mode 100644 index 0000000000..702effa76f --- /dev/null +++ b/railties/lib/rails_generator/manifest.rb @@ -0,0 +1,53 @@ +module Rails + module Generator + + # Manifest captures the actions a generator performs. Instantiate + # a manifest with an optional target object, hammer it with actions, + # then replay or rewind on the object of your choice. + # + # Example: + # manifest = Manifest.new { |m| + # m.make_directory '/foo' + # m.create_file '/foo/bar.txt' + # } + # manifest.replay(creator) + # manifest.rewind(destroyer) + class Manifest + attr_reader :target + + # Take a default action target. Yield self if block given. + def initialize(target = nil) + @target, @actions = target, [] + yield self if block_given? + end + + # Record an action. + def method_missing(action, *args, &block) + @actions << [action, args, block] + end + + # Replay recorded actions. + def replay(target = nil) + send_actions(target || @target, @actions) + end + + # Rewind recorded actions. + def rewind(target = nil) + send_actions(target || @target, @actions.reverse) + end + + # Erase recorded actions. + def erase + @actions = [] + end + + private + def send_actions(target, actions) + actions.each do |method, args, block| + target.send(method, *args, &block) + end + end + end + + end +end diff --git a/railties/lib/rails_generator/options.rb b/railties/lib/rails_generator/options.rb new file mode 100644 index 0000000000..afe2d31625 --- /dev/null +++ b/railties/lib/rails_generator/options.rb @@ -0,0 +1,135 @@ +require 'optparse' +require File.dirname(__FILE__) + '/../support/class_inheritable_attributes' + +module Rails + module Generator + module Options + def self.append_features(base) + super + base.extend(ClassMethods) + class << base + if respond_to?(:inherited) + alias_method :inherited_without_options, :inherited + end + alias_method :inherited, :inherited_with_options + end + end + + module ClassMethods + def inherited_with_options(sub) + inherited_without_options(sub) if respond_to?(:inherited_without_options) + sub.extend(Rails::Generator::Options::ClassMethods) + end + + def mandatory_options(options = nil) + if options + write_inheritable_attribute(:mandatory_options, options) + else + read_inheritable_attribute(:mandatory_options) or write_inheritable_attribute(:mandatory_options, {}) + end + end + + def default_options(options = nil) + if options + write_inheritable_attribute(:default_options, options) + else + read_inheritable_attribute(:default_options) or write_inheritable_attribute(:default_options, {}) + end + end + + # Merge together our class options. In increasing precedence: + # default_options (class default options) + # runtime_options (provided as argument) + # mandatory_options (class mandatory options) + def full_options(runtime_options = {}) + default_options.merge(runtime_options).merge(mandatory_options) + end + + end + + # Each instance has an options hash that's populated by #parse. + def options + @options ||= {} + end + attr_writer :options + + protected + # Convenient access to class mandatory options. + def mandatory_options + self.class.mandatory_options + end + + # Convenient access to class default options. + def default_options + self.class.default_options + end + + # Merge together our instance options. In increasing precedence: + # default_options (class default options) + # options (instance options) + # runtime_options (provided as argument) + # mandatory_options (class mandatory options) + def full_options(runtime_options = {}) + self.class.full_options(options.merge(runtime_options)) + end + + # Parse arguments into the options hash. Classes may customize + # parsing behavior by overriding these methods: + # #banner Usage: ./script/generate [options] + # #add_options! Options: + # some options.. + # #add_general_options! General Options: + # general options.. + def parse!(args, runtime_options = {}) + self.options = {} + + @option_parser = OptionParser.new do |opt| + opt.banner = banner + add_options!(opt) + add_general_options!(opt) + opt.parse!(args) + end + + self.options = full_options(runtime_options) + args + end + + # Raise a usage error. Override usage_message to provide a blurb + # after the option parser summary. + def usage + raise UsageError, "#{@option_parser}\n#{usage_message}" + end + + def usage_message + '' + end + + # Override with your own usage banner. + def banner + "Usage: #{$0} [options]" + end + + # Override with a method that adds options to the parser. + # Added options should use the options hash. For example, + # def add_options!(opt) + # opt.on('-v', '--verbose') { |value| options[:verbose] = value } + # end + def add_options!(opt) + end + + # Adds general options like -h and --quiet. Usually don't override. + def add_general_options!(opt) + opt.separator '' + opt.separator 'General Options:' + + opt.on('-p', '--pretend', 'Run but do not make any changes.') { |options[:pretend]| } + opt.on('-f', '--force', 'Overwrite files that already exist.') { options[:collision] = :force } + opt.on('-s', '--skip', 'Skip files that already exist.') { options[:collision] = :skip } + opt.on('-q', '--quiet', 'Keep is like a secret with /dev/null.') { |options[:quiet]| } + opt.on('-t', '--backtrace', 'Debugging: show backtrace on errors.') { |options[:backtrace]| } + opt.on('-h', '--help', 'Show this help message.') { |options[:help]| } + end + + end + end +end diff --git a/railties/lib/rails_generator/scripts.rb b/railties/lib/rails_generator/scripts.rb new file mode 100644 index 0000000000..007980dcb5 --- /dev/null +++ b/railties/lib/rails_generator/scripts.rb @@ -0,0 +1,81 @@ +require File.dirname(__FILE__) + '/options' + +module Rails + module Generator + module Scripts + + # Generator scripts handle command-line invocation. Each script + # responds to an invoke! class method which handles option parsing + # and generator invocation. + class Base + include Options + default_options :collision => :ask, :quiet => false + + # Run the generator script. Takes an array of unparsed arguments + # and a hash of parsed arguments, takes the generator as an option + # or first remaining argument, and invokes the requested command. + def run(args = [], runtime_options = {}) + begin + parse!(args, runtime_options) + + # Generator name is the only required option. + unless options[:generator] + usage if args.empty? + options[:generator] ||= args.shift + end + + # Look up generator instance and invoke command on it. + Rails::Generator::Base.instance(options[:generator], args, options).command(options[:command]).invoke! + rescue => e + puts e + puts " #{e.backtrace.join("\n ")}\n" if options[:backtrace] + raise SystemExit + end + end + + protected + # Override with your own script usage banner. + def banner + "Usage: #{$0} [options] generator [args]" + end + + def usage_message + usage = "\nInstalled Generators\n" + Rails::Generator::Base.sources.each do |source| + label = source.label.to_s.capitalize + names = source.names + usage << " #{label}: #{names.join(', ')}\n" unless names.empty? + end + + usage << < :destroy + + protected + def add_options!(opt) + end + end +end diff --git a/railties/lib/rails_generator/scripts/generate.rb b/railties/lib/rails_generator/scripts/generate.rb new file mode 100644 index 0000000000..329d6691df --- /dev/null +++ b/railties/lib/rails_generator/scripts/generate.rb @@ -0,0 +1,11 @@ +require File.dirname(__FILE__) + '/../scripts' + +module Rails::Generator::Scripts + class Generate < Base + mandatory_options :command => :create + + protected + def add_options!(opt) + end + end +end diff --git a/railties/lib/rails_generator/scripts/update.rb b/railties/lib/rails_generator/scripts/update.rb new file mode 100644 index 0000000000..ad1ae8004a --- /dev/null +++ b/railties/lib/rails_generator/scripts/update.rb @@ -0,0 +1,15 @@ +require File.dirname(__FILE__) + '/../scripts' + +module Rails::Generator::Scripts + class Update < Base + mandatory_options :command => :update + + protected + def add_options!(opt) + end + + def banner + "Usage: #{$0} [options] scaffold" + end + end +end diff --git a/railties/lib/rails_generator/simple_logger.rb b/railties/lib/rails_generator/simple_logger.rb new file mode 100644 index 0000000000..d750f07b84 --- /dev/null +++ b/railties/lib/rails_generator/simple_logger.rb @@ -0,0 +1,46 @@ +module Rails + module Generator + class SimpleLogger # :nodoc: + attr_reader :out + attr_accessor :quiet + + def initialize(out = $stdout) + @out = out + @quiet = false + @level = 0 + end + + def log(status, message, &block) + @out.print("%12s %s%s\n" % [status, ' ' * @level, message]) unless quiet + indent(&block) if block_given? + end + + def indent(&block) + @level += 1 + if block_given? + begin + block.call + ensure + outdent + end + end + end + + def outdent + @level -= 1 + if block_given? + begin + block.call + ensure + indent + end + end + end + + private + def method_missing(method, *args, &block) + log(method.to_s, args.first, &block) + end + end + end +end diff --git a/railties/lib/rails_generator/spec.rb b/railties/lib/rails_generator/spec.rb new file mode 100644 index 0000000000..ad609b8fe3 --- /dev/null +++ b/railties/lib/rails_generator/spec.rb @@ -0,0 +1,44 @@ +module Rails + module Generator + # A spec knows where a generator was found and how to instantiate it. + # Metadata include the generator's name, its base path, and the source + # which yielded it (PathSource, GemSource, etc.) + class Spec + attr_reader :name, :path, :source + + def initialize(name, path, source) + @name, @path, @source = name, path, source + end + + # Look up the generator class. Require its class file, find the class + # in ObjectSpace, tag it with this spec, and return. + def klass + unless @klass + require class_file + @klass = lookup_class + @klass.spec = self + end + @klass + end + + def class_file + "#{path}/#{name}_generator.rb" + end + + def class_name + "#{name.camelize}Generator" + end + + private + # Search for the first Class descending from Rails::Generator::Base + # whose name matches the requested class name. + def lookup_class + ObjectSpace.each_object(Class) do |obj| + return obj if obj.ancestors.include?(Rails::Generator::Base) and + obj.name.split('::').last == class_name + end + raise NameError, "Missing #{class_name} class in #{class_file}" + end + end + end +end -- cgit v1.2.3