やりたいこと
devise: https://github.com/plataformatec/devise
$ rails generate devise MODEL
みたいに、自分の作成しているgemにgenerate
コマンドをつけたい。
基本的なこと:ジェネレータの基本を理解する
###サンプル
class InitializerGenerator < Rails::Generators::NamedBase
desc "このジェネレータはconfig/initializersにイニシャライザファイルを作成する"
source_root File.expand_path("../templates", __FILE__)
def copy_initializer_file
copy_file "initializer.rb", "config/initializers/#{file_name}.rb"
end
end
テンプレートファイルを作成しておく
puts "hoge"
コマンドを実行する。
$ ails generate initializer core_extensions
config/initializers/#{file_name}.rb
で指定したとおり、config/initializers/core_extensions.rb
が作成されます。
###ポイント
-
class InitializerGenerator < Rails::Generators::NamedBase
で定義されたメソッドは全て実行される。 -
Rails::Generators::NamedBase
は、rails generateコマンドの時に引数を1つもつことを前提とする。 - 引数がいらない場合、
Rails::Generators::Base
を継承してclass定義を行う -
copy_file
は、Module: Thor::Actionsで定義されています。 -
file_name
メソッドはRails::Generators::NamedBaseを継承することにより利用できます
(Rails::Generators::Baseでは利用できません)。
##Railsのバージョンが固定されている場合にmigrationファイルを作成する
require 'rails/generators/active_record'
class InitializerGenerator < ActiveRecord::Generators::Base
desc "このジェネレータはmigration.rbをdb/migration/にコピーします"
source_root File.expand_path("../templates", __FILE__)
def copy_devise_migration
migration_template "migration.rb", "db/migrate/test_create_#{table_name}.rb"
end
end
コマンドを実行します。
$ rails g initializer Hoge
以下にlib/generators/templates/migration.rb
で定義したファイルがコピーされます。
db/migrate/20180502231441_test_create_hoges.rb
以上。
deviseは、様々なRailsのバージョンで使用されるので、migrationを作成する段階と、
migrationファイルそれ自身を工夫する必要があります。
※ migration_template: migration versionがコピー先ファイル名に付け加えられることがポイントです(意訳)
##migrationファイルを作成する
ここは、Rails5.2で実際に作成されたmigrationファイルをまず見てみます。
class CreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.string :name
t.integer :age
t.timestamps
end
end
end
ポイントは、[5.2]みたいに、Railsのバージョンを指定してあげる必要があることです。
それに対処するために、次のようにdeviseでは定義されています。
def copy_devise_migration
migration_template "migration.rb", "#{migration_path}/devise_create_#{table_name}.rb", migration_version: migration_version
end
def rails5?
Rails.version.start_with? '5'
end
def migration_version
if rails5?
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
end
end
def migration_path
if Rails.version >= '5.0.3'
db_migrate_path
else
@migration_path ||= File.join("db", "migrate")
end
end
##migrationファイルの定義
deviseではこんな感じに定義されています。
class TestCreate<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
def change
create_table :<%= table_name %> do |t|
<%= migration_data -%>
end
end
end
migraion_dataに関しては、単純にヒアドキュメントを返すだけです。
def migration_data
binding.pry
<<Ruby
t.string :name
t.integer :age
t.timestamps
Ruby
end
ORM
実はこの時点ではまだORM(Object-relational mapping)は行われていません。
(rails db:migrate
しても、app/model下に対象のファイルが作られていないことがわかります。)
deviseの実装を参考にして、以下の記述を追加します。
def generate_model
invoke "active_record:model", [name], migration: false unless model_exists? && behavior == :invoke
end
def model_exists?
File.exist?(File.join(destination_root, model_path))
end
def model_path
@model_path ||= File.join("app", "models", "#{file_path}.rb")
end
migrationファイルは独自に作成するので、Modelだけ作成しています。
def generate_model
invoke "active_record:model", [name], migration: false unless model_exists? && behavior == :invoke
end
以下は同じような動きになります。
$ rails g model User --skip-migration
app/model下にファイルが生成されることでORMが完了します。
まとめ
code: https://gist.github.com/chamao/98c816a9b366f697d871d794795c9d87
$ rails g initializer Sample
$ rails db:migrate
ちゃんと目的のモデルが生成されていることがわかります。
$ rails c
Sample.create(name: "test", age:10 )
argument
今回は使わなかったけどdeviseで定義されているもの。
argument :attributes, type: :array, default: [], banner: "field:type field:type"
例えば、以下のようなコマンドに対応するため。
rails g devise User address:string age:integer
address:string age:integer
の部分が、attributes
に格納され、
migrationファイルでは以下のように展開されている。
<% attributes.each do |attribute| -%>
t.<%= attribute.type %> :<%= attribute.name %>
<% end -%>
class_option
今回は使わなかったけどdeviseで定義されているもの。
class_option :primary_key_type, type: :string, desc: "The type for primary key"
primary_key_typeはメソッドになっていて、こんな感じに定義されている。
def primary_key_type
primary_key_string if rails5?
end
def primary_key_string
key_string = options[:primary_key_type]
", id: :#{key_string}" if key_string
end
これが、migrationファイルでは以下のように展開されている。
create_table :<%= table_name %><%= primary_key_type %> do |t|
例えば、primary_keyをstring型に変更したい場合はこんな感じにする。
class_option :primary_key_type, type: :string, default: "string" , desc: "The type for primary key"
class_optionに関しては下記の記事が分かりやすかったです。
Thorの使い方まとめ
クラス全体で共通のオプションをclass_optionで指定することができます。
参照
Rails ジェネレータとテンプレート入門
migration_template(source, destination, config = {})