Rails generator, Thor

Rails Guideのコマンドラインのところを見ているとgeneratorを作成できることに気づいた

```sh
  # こんな感じでファイルをテンプレートから作れるジェネレータを作れる
  be rails g my_generator hogehoge
```

config/defsは作成するのが面倒だと前から感じていた

---

# generatorを作ってみた

---

defsに定数を追加したいときは大体以下のファイルを生成する必要がある

- config/defs/**.yml
- app/model/defs/**.rb
- frontend/src/Defs/**.jsx

---

こんな感じでテンプレートを生成したい

```sh
  be rails g defs hoge_fuga

  Running via Spring preloader in process 175
    create  app/models/defs/hoge_fuga.rb
    create  config/defs/hoge_fuga.yml
    create  frontend/src/Defs/HogeFuga.jsx
```

---

# generatorをgenerateする

---

### generatorをコマンドから作成する

```sh
  # testも作成されるが削除した
  be rails g generator defs

    create    lib/generators/defs/defs_generator.rb
    create    lib/generators/defs/USAGE
    create    lib/generators/defs/templates
```

---

### 各ファイルの説明

- defs_generator.rb

generatorの本体。

- USAGE

helpの中身`be rails g defs --help`で中身を表示する
説明は省略する

- templates

template用のerbを置くディレクトリ

---

### generator本体

こいつのパブリックメソッドが定義順に実行される
Thorというgemを使っている
Rails::Generators::NamedBaseを継承するとデフォルトでnameに引数が入る

```rb
class DefsGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('templates', __dir__)

  def create_model_file
    template 'model.rb.erb', "app/models/defs/#{name.underscore}.rb"
  end

  def create_config_file
    template 'config.yml.erb', "config/defs/#{name.underscore}.yml"
  end

  def create_jsx_file
    template 'front_defs.jsx.erb', "frontend/src/Defs/#{class_name}.jsx"
  end

  private

  def class_name
    name.classify
  end
end

```

--- 

### templates

普通のerbで書ける

model.rb.erb

```erb
class Defs::<%= class_name %> < ActiveYaml::Base
  include ActiveHash::Enum

  set_root_path Rails.root.join('config/defs')
  set_filename '<%= name.underscore %>'

  enum_accessor :key
end
```

config.yml.erb

```erb
#
# id <Int>
# key: <String>
# name <String>
#
hoge:
  id: 1
  key: hoge
  name: fuga
```

front_defs.jsx.erb

```erb
import constantizeJSONDef from 'ConstantizeJSONDef';
import Def from 'RailsConfig/defs/<%= name.underscore %>.yml';

export default constantizeJSONDef(Def, {
  key: 'key',
  name: '<%= class_name %>',
});
```
---

### Usage

```sh
> be rails g defs --help
Usage:
  rails generate defs NAME [options]

Options:
  [--skip-namespace], [--no-skip-namespace]              # Skip namespace (affects only isolated engines)
  [--skip-collision-check], [--no-skip-collision-check]  # Skip collision check

Runtime options:
  -f, [--force]                    # Overwrite files that already exist
  -p, [--pretend], [--no-pretend]  # Run but do not make any changes
  -q, [--quiet], [--no-quiet]      # Suppress status output
  -s, [--skip], [--no-skip]        # Skip files that already exist

Description:
    Defsを生成する

Example:
    be rails g defs hoge

    This will create:
        app/models/defs/hoge.rb
        config/defs/hoge.yml
        frontend/src/Defs/Hoge.jsx

    消したくなったら
    be rails destroy defs hoge

```

---

# Railsのgeneratorをもう少し詳しく

---

# Thor

---

Railsのgeneratorの内部で使われているgem
コマンドラインツールの作成のためのgem
Rake-likeなシンタックスで書ける

Rails内部で使われているのでrequireすれば使える

---

引数の扱いがrakeと比べて楽

```rb
class HogeGenerator < Rails::Generators::Base
  source_root File.expand_path('templates', __dir__)

  # documentには :string, :numeric, :array, :hashが使えるとある
  # :optionalと:requiredは逆の関係にある
  argument :arg1, type: :string, default: 'str', desc: 'essential argment'
  argument :arg3, type: :hash, optional: true
  argument :arg4, type: :array, required: false
  # 何故かバグった
  # argument :arg2, type: :numeric

  def test_args
    puts arg1
    # puts arg2
    puts arg3
    puts arg4
  end

  class_option :skip_option, type: :boolean, default: false, desc: 'this is description'
  class_option :put_option2, type: :boolean, default: false, desc: 'this is description', aliases: [:x, :y, :z]

  def test_class_options
    say 'not skip option' unless options[:skip_option]
    puts 'put option2' if options[:put_option2]
  end
end
```

---

RailsのgeneratorではThor::GroupとThor::Actionsを拡張する形で使っている
Thor::Groupはpublicメソッドを定義順に呼び出す処理を主に行っている
Actionsはrailsのものと合わせてdocを探しながら実装するのが良さそう

thor_doc: 
https://rdoc.info/github/erikhuda/thor/master/Thor/Actions

rails_doc:
https://api.rubyonrails.org/classes/Rails/Generators/Actions.html

```rb
module Rails
  module Generators
    class Error < Thor::Error # :nodoc:
    end

    class Base < Thor::Group
      include Thor::Actions
      include Rails::Generators::Actions

  
　# 以下略
end
```

---

使えそうなメソッド

### Thor::Actions

- directory(source, *args, &block)
ディレクトリごと再帰的にコピーする

- gsub_file(path, flag, *args, &block)
ファイル内のテキストを置き換える

- inject_into_class(path, klass, *args, &block) ⇒ Object
- inject_into_module(path, module_name, *args, &block) ⇒ Object
- insert_into_file(destination, *args, &block) ⇒ Object (also: #inject_into_file)
読んで字の如くコードの内容を挿入できる

- inside(dir = "", config = {}, &block)
ブロック実行中のみ一時的に他ディレクトリをルートにする

- run(command, config = {})
シェルのコマンドが実行できる

- run_ruby_script(command, config = {})
rubyのスクリプトが実行できる
- template(source, *args, &block)
ERBのテンプレートからファイルを生成できる

### Rails::Generators::Actions

- rake(command, options = {})
rakeタスクが実行できる

- git(commands = {})
gitコマンドが実行できる

---