aboutsummaryrefslogtreecommitdiffstats
path: root/railties/lib/rails_generator.rb
blob: 83a404fc0afdd4668d1179e86689fc7302b3af84 (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
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