Rails のGeneratorのデフォルト値はどこから来るのか
ことの発端
Rails のController generator で不要なrouting が書き込まれてしまうので、毎度--skip-routes
オプションを付けるようにしている。
けれど、オプションを付け忘れることもあって、面倒なことになることがよくあった。
調べてみると、 config/application.rb
などでconfig.generators
を以下のように指定することでデフォルトのオプションとして指定できることがわかった。
config.generators do |g| g.skip_routes = true end
該当の変更だけを見ても、仕組みが理解できなかったので調べてみた。
TL;DR
controller generator にオプションの渡し方は、以下の3通り。
config.generators.controller = { skip_routes: true }
config.generators.rails = { skip_routes: true }
config.generators.skip_routes = true
個別のgeneratorのためのオプションとして最優先されるのは1の設定の仕方。 2と3は同等の扱いで、他のgeneratorとも共有される。
探求編
コードを読んでいく方針として次の順で行った。
- 参照側:
ControllerGenerator
のoptions[:skip_routes]
はどこを見ているのか - 設定側:
Rails::Application.config.generators
に指定した値はどこに入るのか - 上記2つはどこでつながるのか
参照側
module Rails module Generators class ControllerGenerator < NamedBase # :nodoc: argument :actions, type: :array, default: [], banner: "action action" class_option :skip_routes, type: :boolean, desc: "Don't add routes to config/routes.rb." # # 中略 # def add_routes return if options[:skip_routes] return if actions.empty? routing_code = actions.map { |action| "get '#{file_name}/#{action}'" }.join("\n") route routing_code, namespace: regular_class_path end
add_routes
メソッド内で、options[:skip_routes]
を見ているので、options
がどこから来るのかを探る。
ControllerGenerator
自身は持っていないので、継承しているNamedBase
を見てみるがこれもoptions
を持っていない。更に上位クラスのRails::Generators::Base
を見ると、これがThor::Group
を継承していることがわかる。
RailsのgeneratorなどCLIは、CLIを作成するDSLを提供するthorを使用している。
Thorのドキュメントを見ると、Thor::Group
がincludeしているモジュールThor::Base
にoptions
メソッドが存在していることがわかる。
ControllerGenerator
のコードを再度見ていくと、skip_routes
オプションをclass_option
で定義している。
これもまたthorに存在するメソッドである。しかし、デフォルト値を指定するためのdefault
をキーに持つハッシュが引数に渡されていない。
Rails::Generators::Base
を見ると、class_option
メソッドをオーバーライドしている箇所がある。
def self.class_option(name, options = {}) #:nodoc: options[:desc] = "Indicates when to generate #{name.to_s.humanize.downcase}" unless options.key?(:desc) options[:aliases] = default_aliases_for_option(name, options) options[:default] = default_value_for_option(name, options) super(name, options) end
これは、Thor::Base.class_option
をsuper
で呼ぶ前に、デフォルト値を設定している。
default_value_for_option
は以下のようになっている。
def self.default_value_for_option(name, options) # :doc: default_for_option(Rails::Generators.options, name, options, options[:default]) end
さらにdefault_for_option
を呼び出している。
def self.default_for_option(config, name, options, default) # :doc: if generator_name && (c = config[generator_name.to_sym]) && c.key?(name) c[name] elsif base_name && (c = config[base_name.to_sym]) && c.key?(name) c[name] elsif config[:rails].key?(name) config[:rails][name] else default end end
これは以下のようにデフォルト値を決めている。
config
(ここだとRails::Generators.options
)の中にgenerator名(ここだとcontroller
)をキーとして持っていて、なおかつその値がハッシュでname
(ここだとskip_routes
)をキーとして持っていると、その値をデフォルト値とするconfig
の中にbase_name(ここだとrails
)をキーとして持っていて、なおかつその値がハッシュでname
をキーとして持っていると、その値をデフォルト値とするconfig
の中にシンボルrails
をキーとして持っていて、なおかつその値がハッシュでありそのハッシュがname
をキーとして持っている場合、その値をデフォルト値とする- 以上のどれにも当てはまらない場合、引数の
default
をデフォルト値とする
つまり、Rails::Generators.options
がどのようになっているのかで決まる。
この順番で先に見つかった値が使われる。
設定編
Rails Guideに、Generatorの設定についての記述があり、そこに下記のサンプルコードがある。
config.generators do |g| g.orm :active_record g.test_framework :test_unit end
Rails::Engine::Configuration#generators
を見ると、
def generators @generators ||= Rails::Configuration::Generators.new yield(@generators) if block_given? @generators end
Rails::Configuration::Generators
をブロックに渡している。
Rails::Configuration::Generators
はmethod_missing
が定義されていて、任意の名前で設定値を与えることができる。
下記のように4通りの記述が可能である。
config.generators.controller = { skip_routes: true }
config.generators.controller skip_routes: true
config.generators.rails = { skip_routes: true }
config.generators.skip_routes = true
1、2は同じ扱いで、controller generator固有のオプションを設定している。 3、4もまた同じ扱いで、controller generatorのオプションのデフォルト値だけでなく他のgeneratorからも参照される可能性がある。 「可能性がある」というのは、先述のようにgenerator固有の設定(つまり上の1、2で指定された値)を優先するためである。
どこでつながるのか
これで一件落着のように感じるが、Rails::Generators.options
の中を見ると、以下のようにDEFAULT_OPTIONS
にあるハッシュを返しているだけである。
def options #:nodoc: @options ||= DEFAULT_OPTIONS.dup end
rails generator
コマンド実行時にRails::Configuration::Generators
に設定された内容をRails::Generators.options
に入れている。
rails/generate_command.rb at c0d91a4f9da10094ccdb80e34d1be42ce1016c9a · rails/rails · GitHub
このload_generators
はRails.application.load_generators
を呼んでいるだけのメソッドである。
Rails.application.load_generators
の実体はRails::Engine#load_generators
で、この中で下記を実行している。
Rails::Generators.configure!(app.config.generators)
引数のapp.config.generators
は、設定編で解説した設定で生成したRails::Generators
オブジェクトである。
レシーバーのconfigure!
の中で、Rails::Generators.options
の値に、Rails::Configuration::Generators.options
の値をmerge
している。
ということで、このように設定した値がController Generatorから参照されていることがわかった。
Generatorを自分で作成する必要がありそのGenerator固有のオプションを持つ場合、そのオプションのデフォルト値はconfig.generators.my_generator = { my_option: 'foobar' }
といったように作成したgenerator名に値を与えることで設定できる。