ジェネレータ：自作のmigraionファイルを生成する

## やりたいこと

devise: https://github.com/plataformatec/devise

```
$ rails generate devise MODEL
```

みたいに、自分の作成しているgemに`generate`コマンドをつけたい。

## 基本的なこと：ジェネレータの基本を理解する

###サンプル

[Rails ジェネレータとテンプレート入門](https://railsguides.jp/generators.html)

```ruby:lib/generators/initializer_generator.rb
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
```

テンプレートファイルを作成しておく

```ruby:lib/generators/templates/initializer.rb
puts "hoge"
```

コマンドを実行する。

```bash:terminal
$ 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コマンドの時に引数を１つもつことを前提とする。
- 引数がいらない場合、`Rails::Generators::Base`を継承してclass定義を行う
- `copy_file`は、[Module: Thor::Actions](https://www.rubydoc.info/github/erikhuda/thor/master/Thor/Actions.html)で定義されています。
- `file_name`メソッドはRails::Generators::NamedBaseを継承することにより利用できます
（Rails::Generators::Baseでは利用できません）。


##Railsのバージョンが固定されている場合にmigrationファイルを作成する

```ruby:lib/generators/templates/initializer.rb
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](https://apidock.com/rails/Rails/Generators/Migration/migration_template)：　migration versionがコピー先ファイル名に付け加えられることがポイントです（意訳）



##migrationファイルを作成する

ここは、Rails5.2で実際に作成されたmigrationファイルをまず見てみます。

```ruby:db/migrate/20180502230333_create_users.rb
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では定義されています。


```ruby
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ではこんな感じに定義されています。

```ruby
class TestCreate<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
  def change
    create_table :<%= table_name %> do |t|
<%= migration_data -%>
    end
  end
end
```

migraion_dataに関しては、単純にヒアドキュメントを返すだけです。

```ruby:lib/generators/templates/initializer.rb
  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の実装を参考にして、以下の記述を追加します。

```ruby:lib/generators/templates/initializer.rb
  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だけ作成しています。

```bash:terminal
def generate_model
  invoke "active_record:model", [name], migration: false unless model_exists? && behavior == :invoke
end
```

以下は同じような動きになります。

```bash:terminal
$ 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で定義されているもの。

```ruby
argument :attributes, type: :array, default: [], banner: "field:type field:type"
```

例えば、以下のようなコマンドに対応するため。

```
rails g devise User address:string age:integer 
```

`address:string age:integer`の部分が、`attributes`に格納され、
migrationファイルでは以下のように展開されている。

```ruby
<% attributes.each do |attribute| -%>
  t.<%= attribute.type %> :<%= attribute.name %>
<% end -%>
```

## class_option
今回は使わなかったけどdeviseで定義されているもの。

```ruby 
class_option :primary_key_type, type: :string, desc: "The type for primary key"
```

primary_key_typeはメソッドになっていて、こんな感じに定義されている。

```ruby
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ファイルでは以下のように展開されている。

```ruby
create_table :<%= table_name %><%= primary_key_type %> do |t|
```

例えば、primary_keyをstring型に変更したい場合はこんな感じにする。

```ruby
class_option :primary_key_type, type: :string, default: "string" , desc: "The type for primary key"
```

class_optionに関しては下記の記事が分かりやすかったです。
[Thorの使い方まとめ](https://qiita.com/succi0303/items/32560103190436c9435b)
> クラス全体で共通のオプションをclass_optionで指定することができます。



## 参照
[Rails ジェネレータとテンプレート入門](https://railsguides.jp/generators.html)
[migration_template(source, destination, config = {})](http://api.rubyonrails.org/v5.0/classes/Rails/Generators/Migration.html#method-i-migration_template)