[[Ruby on Rails4を読む]] #contents *はじめに [#n73ec655] 前回はrails new時の挙動について見てきました。railsコマンドは初めにアプリケーションを作成するとき以外にも rails generate scaffold user name:string email:string のように使用します。今回はこの場合にどういう処理が行われるのか見ていきます。 *Rails::AppRailsLoader (railties/lib/rails/app_rails_loader.rb) [#j469bed6] さて、前回も見たrails/cli.rbです。 #code(Ruby){{ require 'rails/app_rails_loader' # If we are inside a Rails application this method performs an exec and thus # the rest of this script is not run. Rails::AppRailsLoader.exec_app_rails require 'rails/ruby_version_check' Signal.trap("INT") { puts; exit(1) } if ARGV.first == 'plugin' ARGV.shift require 'rails/commands/plugin' else require 'rails/commands/application' end }} 前回は下部のrails/commands/application.rbに進みました。今回は前回無視したapp_rails_loader.rbに進みます。 #code(Ruby){{ module Rails module AppRailsLoader RUBY = Gem.ruby EXECUTABLES = ['bin/rails', 'script/rails'] def self.exec_app_rails original_cwd = Dir.pwd loop do if exe = find_executable contents = File.read(exe) if contents =~ /(APP|ENGINE)_PATH/ exec RUBY, exe, *ARGV break # non reachable, hack to be able to stub exec in the test suite elsif exe.end_with?('bin/rails') && contents.include?('This file was generated by Bundler') $stderr.puts(BUNDLER_WARNING) Object.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd)) require File.expand_path('../boot', APP_PATH) require 'rails/commands' break end end # If we exhaust the search there is no executable, this could be a # call to generate a new application, so restore the original cwd. Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root? # Otherwise keep moving upwards in search of an executable. Dir.chdir('..') end end def self.find_executable EXECUTABLES.find { |exe| File.file?(exe) } end }} 何しているかというと、アプリケーションディレクトリのbin/railsを実行しています。<APP_DIR>/bin/railsは以下のようになっています。 #code(Ruby){{ #!/usr/bin/env ruby APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' require 'rails/commands' }} *railties/lib/rails/commands.rb [#vc7d5786] 全部見ると長くなるのでrails/commandsに進みます。 #code(Ruby){{ ARGV << '--help' if ARGV.empty? aliases = { "g" => "generate", "d" => "destroy", "c" => "console", "s" => "server", "db" => "dbconsole", "r" => "runner" } command = ARGV.shift command = aliases[command] || command require 'rails/commands/commands_tasks' Rails::CommandsTasks.new(ARGV).run_command!(command) }} commands_tasks.rbへ。 #code(Ruby){{ module Rails class CommandsTasks # :nodoc: def run_command!(command) command = parse_command(command) if COMMAND_WHITELIST.include?(command) send(command) else write_error_message(command) end end def generate generate_or_destroy(:generate) end def generate_or_destroy(command) require 'rails/generators' require_application_and_environment! Rails.application.load_generators require "rails/commands/#{command}" end }} メソッド呼び出しとかrequireとかしてて知らぬ間にいろいろ設定されてそうですがぱっと見ではよくわからないのでgenerate.rbに進みます。 railties/lib/rails/commands/generate.rb #code(Ruby){{ require 'rails/generators' if [nil, "-h", "--help"].include?(ARGV.first) Rails::Generators.help 'generate' exit end name = ARGV.shift root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root Rails::Generators.invoke name, ARGV, behavior: :invoke, destination_root: root }} 忘れかけているので確認。この時点でのARGVは scaffold user name:string email:string です。 invokeメソッドに進む。 railties/lib/rails/generators.rb #code(Ruby){{ def self.invoke(namespace, args=ARGV, config={}) names = namespace.to_s.split(':') if klass = find_by_namespace(names.pop, names.any? && names.join(':')) args << "--help" if args.empty? && klass.arguments.any? { |a| a.required? } klass.start(args, config) else puts "Could not find generator #{namespace}." end end }} *find_by_namespace (railties/lib/rails/generators.rb) [#q4ea6b8b] そろそろ見出しを変えます。与えられたname(コマンド。namespaceもサポートしてるようだけど今回はnamespaceなしのscaffoldです)からクラスを探しているようです。とりあえずコード。 #code(Ruby){{ def self.find_by_namespace(name, base=nil, context=nil) #:nodoc: lookups = [] lookups << "#{base}:#{name}" if base lookups << "#{name}:#{context}" if context unless base || context unless name.to_s.include?(?:) lookups << "#{name}:#{name}" lookups << "rails:#{name}" end lookups << "#{name}" end lookup(lookups) namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }] lookups.each do |namespace| klass = namespaces[namespace] return klass if klass end invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name) end }} loookupsは["scaffold/scaffold", "rails/scaffold", "scaffold"]です。 #code(Ruby){{ def self.lookup(namespaces) #:nodoc: paths = namespaces_to_paths(namespaces) paths.each do |raw_path| ["rails/generators", "generators"].each do |base| path = "#{base}/#{raw_path}_generator" begin require path return rescue LoadError => e raise unless e.message =~ /#{Regexp.escape(path)}$/ rescue Exception => e warn "[WARNING] Could not load generator #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}" end end end end def self.namespaces_to_paths(namespaces) #:nodoc: paths = [] namespaces.each do |namespace| pieces = namespace.split(":") paths << pieces.dup.push(pieces.last).join("/") paths << pieces.join("/") end paths.uniq! paths end }} 最終的に、rails/generators/rails/scaffold/scaffold_generator.rbがrequireされます。 *railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb [#n0c3bbf2] #code(Ruby){{ require 'rails/generators/rails/resource/resource_generator' module Rails module Generators class ScaffoldGenerator < ResourceGenerator # :nodoc: }} rails/generators/rails/resource/resource_generator.rb #code(Ruby){{ module Rails module Generators class ResourceGenerator < ModelGenerator # :nodoc: }} rails/generators/rails/model/model_generator.rb #code(Ruby){{ module Rails module Generators class ModelGenerator < NamedBase # :nodoc: argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]" }} rails/generators/named_base.rb #code(Ruby){{ module Rails module Generators class NamedBase < Base argument :name, type: :string }} というわけで、前回見たRails::Generator::Baseまで来ました。NamedBaseでname、ModelGeneratorでattributesが定義されているので、 name: "user" arguments: ["name:string", "email:string"] が設定されます。 さてでは次に実行されるコマンドを・・・と見てみてもそれっぽい定義はありません。代わりにhook_forという怪しいメソッドが呼ばれています。 railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb #code(Ruby){{ module Rails module Generators class ScaffoldGenerator < ResourceGenerator # :nodoc: hook_for :scaffold_controller, required: true }} rails/generators/rails/scaffold_controller/template/controller.rbを見ると明らかにこれが使われていそうです。というわけで以降はどういうからくりでhook_forで登録したものが呼ばれているのかを調べていきたいと思います。 *hook_for (railties/lib/rails/generator/base.rb) [#m8d60d0c] hook_forはRails::Generator::Baseに定義されています。 #code(Ruby){{ def self.hook_for(*names, &block) options = names.extract_options! in_base = options.delete(:in) || base_name as_hook = options.delete(:as) || generator_name names.each do |name| unless class_options.key?(name) defaults = if options[:type] == :boolean { } elsif [true, false].include?(default_value_for_option(name, options)) { banner: "" } else { desc: "#{name.to_s.humanize} to be invoked", banner: "NAME" } end class_option(name, defaults.merge!(options)) end hooks[name] = [ in_base, as_hook ] invoke_from_option(name, options, &block) end end def self.base_name @base_name ||= begin if base = name.to_s.split('::').first base.underscore end end end def self.generator_name @generator_name ||= begin if generator = name.to_s.split('::').last generator.sub!(/Generator$/, '') generator.underscore end end end }} base_name、generator_nameで使われているnameはクラス名を表す文字列なので注意してください。それぞれ、"rails"、"scaffold"になります。 invoke_from_optionはThor::Groupで定義されています。 #code(Ruby){{ def invoke_from_option(*names, &block) # rubocop:disable MethodLength options = names.last.is_a?(Hash) ? names.pop : {} verbose = options.fetch(:verbose, :white) names.each do |name| unless class_options.key?(name) fail ArgumentError, "You have to define the option #{name.inspect} " << "before setting invoke_from_option." end invocations[name] = true invocation_blocks[name] = block if block_given? class_eval <<-METHOD, __FILE__, __LINE__ def _invoke_from_option_#{name.to_s.gsub(/\W/, "_")} return unless options[#{name.inspect}] value = options[#{name.inspect}] value = #{name.inspect} if TrueClass === value klass, command = self.class.prepare_for_invocation(#{name.inspect}, value) if klass say_status :invoke, value, #{verbose.inspect} block = self.class.invocation_blocks[#{name.inspect}] _invoke_for_class_method klass, command, &block else say_status :error, %(\#{value} [not found]), :red end end METHOD end end }} 一目見ただけで邪悪さ120%なコードです。このメソッドが実行されるとnameに応じた_invoke_from_options_#{name}というメソッドが定義されます。で、[[前回>Ruby on Rails4/ファイル生成周りを読む/rails new時に行われる処理]]から読んでいただいてる人はご存知だと思いますが、publicなメソッドはコマンドとして実行されるので、hook_forを記述することにより実行されるコマンドの定義ができたということになります。 prepare_for_invocationはnameで指定されたものに対応するクラスを探しています。このメソッドはthor/invocation.rbに書かれていて・・・、というのは引っかけです。私も初めthorの方にあるprepare_for_invocationを(grepで引っかけて)見ていたのですがどう読み進めても先に進めない(scaffold_controllerが実行されない)のでrailtiesでgrepしたところ、rails/generator/base.rbでオーバーライドされていました。 #code(Ruby){{ def self.prepare_for_invocation(name, value) #:nodoc: return super unless value.is_a?(String) || value.is_a?(Symbol) if value && constants = self.hooks[name] value = name if TrueClass === value Rails::Generators.find_by_namespace(value, *constants) elsif klass = Rails::Generators.find_by_namespace(value) klass else super end end }} ifは一番上が実行されます。まあともかくscaffold_controller_generator.rbにたどり着きます。 *orm [#o94b4b7b] ところでここでrails generate scaffoldの出力を見てみましょう。 rails generate scaffold user name:string email:string invoke active_record create db/migrate/20140809033025_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml invoke resource_route route resources :users invoke scaffold_controller create app/controllers/users_controller.rb invoke erb create app/views/users create app/views/users/index.html.erb create app/views/users/edit.html.erb create app/views/users/show.html.erb create app/views/users/new.html.erb create app/views/users/_form.html.erb invoke test_unit create test/controllers/users_controller_test.rb invoke helper create app/helpers/users_helper.rb invoke test_unit create test/helpers/users_helper_test.rb invoke jbuilder create app/views/users/index.json.jbuilder create app/views/users/show.json.jbuilder invoke assets invoke coffee create app/assets/javascripts/users.js.coffee invoke scss create app/assets/stylesheets/users.css.scss invoke scss create app/assets/stylesheets/scaffolds.css.scss invokeというのはgenerator呼んでいるということでしょう。またインデントはgeneratorのネストを表しているのでしょう。 さて、上記の出力結果とScaffoldGenerator、並びにスーパークラスを眺めるとスーパークラスに定義されたもの(メソッド定義が早い順)に実行されていることがわかります。 #code(Ruby){{ class ScaffoldGenerator < ResourceGenerator # :nodoc: remove_hook_for :resource_controller hook_for :scaffold_controller, required: true hook_for :assets do |assets| invoke assets, [controller_name] end hook_for :stylesheet_engine do |stylesheet_engine| if behavior == :invoke invoke stylesheet_engine, [controller_name] end end class ResourceGenerator < ModelGenerator # :nodoc: hook_for :resource_controller, required: true do |controller| invoke controller, [ controller_name, options[:actions] ] end hook_for :resource_route, required: true class ModelGenerator < NamedBase # :nodoc: hook_for :orm, required: true }} で、hook_for :ormと書いてあるのになんでactive_recordが呼ばれてんの? とりあえずormでgrepかけるとrails/engine.rbに以下のコメントが見つかります。 #code(Ruby){{ # == Generators # # You can set up generators for engines with <tt>config.generators</tt> method: # # class MyEngine < Rails::Engine # config.generators do |g| # g.orm :active_record # g.template_engine :erb # g.test_framework :test_unit # end # end # # You can also set generators for an application by using <tt>config.app_generators</tt>: # # class MyEngine < Rails::Engine # # note that you can also pass block to app_generators in the same way you # # can pass it to generators method # config.app_generators.orm :datamapper # end }} 普通に考えるとormのgeneratorとしてactive_recordが設定されているということになります。もう、「以上、終わり!」としたいとこですが、悲しいけどこれコードリーディング記事なのよね、ってことで、ではコメントではなく実際なとこ何処でorm = active_recordが設定されているのかを探してみたいと思います。 gems全体をgrepすると以下のコードが引っ掛かります。 activerecord/lib/active_record/railtie.rb #code(Ruby){{ module ActiveRecord # = Active Record Railtie class Railtie < Rails::Railtie # :nodoc: config.active_record = ActiveSupport::OrderedOptions.new config.app_generators.orm :active_record, :migration => true, :timestamps => true }} 見つかりました。ちなみにこのファイルは前半読み飛ばした以下のところでrequireされています。 railties/lib/rails/commands/commands_tasks #code(Ruby){{ module Rails class CommandsTasks # :nodoc: def generate_or_destroy(command) require 'rails/generators' require_application_and_environment! Rails.application.load_generators require "rails/commands/#{command}" end def require_application_and_environment! require APP_PATH Rails.application.require_environment! end }} <APP_DIR>/bin/rails #code(Ruby){{ APP_PATH = File.expand_path('../../config/application', __FILE__) }} <APP_DIR>/config/applications #code(Ruby){{ require 'rails/all' }} railties/lib/rails/all.rb #code(Ruby){{ require "rails" %w( active_record action_controller action_view action_mailer rails/test_unit sprockets ).each do |framework| begin require "#{framework}/railtie" rescue LoadError end end }} 今まで気にせずにrsiltieという単語を使ってきましたが、railtieというのはRailsを拡張する仕組み(基本セットも同じ概念上に乗っている)のようです。 #code(Ruby){{ # Railtie is the core of the Rails framework and provides several hooks to extend # Rails and/or modify the initialization process. # # Every major component of Rails (Action Mailer, Action Controller, # Action View and Active Record) is a Railtie. Each of # them is responsible for their own initialization. This makes Rails itself # absent of any component hooks, allowing other components to be used in # place of any of the Rails defaults. }} かなり長くなってきましたがまだ続きます。 railties/lib/rails/railtie.rb #code(Ruby){{ module Rails class Railtie class << self delegate :config, to: :instance def instance @instance ||= new end def config @config ||= Railtie::Configuration.new end }} railties/lib/rails/railtie/configuration.rb #code(Ruby){{ module Rails class Railtie class Configuration def app_generators @@app_generators ||= Rails::Configuration::Generators.new yield(@@app_generators) if block_given? @@app_generators end }} ralties/lib/rails/configuration.rb #code(Ruby)){{ module Rails module Configuration class Generators #:nodoc: }} さてと、Rails::Configuration::Generatorsまで来ましたが、ormなんてメソッドありません。まあ、どんな拡張が来るかもわからないのにメソッドなんて用意できませんよね。ではどうするか、そう、みんな大好きmethod_missingです。 #code(Ruby){{ def method_missing(method, *args) method = method.to_s.sub(/=$/, '').to_sym return @options[method] if args.empty? if method == :rails || args.first.is_a?(Hash) namespace, configuration = method, args.shift else namespace, configuration = args.shift, args.shift namespace = namespace.to_sym if namespace.respond_to?(:to_sym) @options[:rails][method] = namespace end if configuration aliases = configuration.delete(:aliases) @aliases[namespace].merge!(aliases) if aliases @options[namespace].merge!(configuration) end end }} というわけで、@options[:rails][:orm]として:active_recordが設定されます。 これで、ormに対してactive_recordが使われるようになりました、・・・というには実はまだもう少し関連付けられているところの確認ができてないのですが、OReMoutsukaretayo、ということで今回はここまでにします。 *おわりに [#c43367ae] というわけでrails generate時の動作を見てきました。今回も黒魔術満載でした。名前に対してファイル探してrequireするとか、コマンドとして呼ばれるpublicメソッドを動的に生成しているとか、伝家の宝刀method_missingとか。 ともかくこれでファイルの生成は終わりました。次回は「rake db:migrate」を見ていきます。これは・・・、多分、今回のよりはましなコードになってると思います、多分。 ともかくこれでファイルの生成は終わりました。次回は「rake db:migrate」を見ていきます。これは・・・、多分、今回のよりはましなコードになってると思います。