読者です 読者をやめる 読者になる 読者になる

scramble cadenza

技術ネタのガラクタ置き場

rails でカスタム generator 作る話

これは何か

好きな generator を作る話。ここで述べる generator とは

rails generator hoge

のような rails generate コマンドのこと。 知識整理のために書いてみる

雛形作成

rails には generator を生成する generator があって、以下の様なコマンドを叩けば良い。

rails generate generator <generator名>

試しに test という generator を作ってみる。

% rails generate generator test                                                                                               
      create  lib/generators/test
      create  lib/generators/test/test_generator.rb
      create  lib/generators/test/USAGE
      create  lib/generators/test/templates
      invoke  test_unit
      create    test/lib/generators/test_generator_test.rb

ファイルが作られた。 作られた generator は以下のように実行できる

rails generate <generator名> (<引数1>, <引数2>, ...)

引数は generator 側の定義で制御する

generator 作成

生成されたファイル

先ほどの rails generate generator test コマンドで生成されたファイルを順番に見ていく

lib/generators/test/test_generator.rb

class TestGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('../templates', __FILE__)
end

メインの generator ファイル。 Rails::Generators::NamedBase を継承した {generator名}Generator というクラスが生成されている。

ここにメソッドを定義していく。

具体例

TestGenerator に何も生産しないメソッドを作る。

class TestGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('../templates', __FILE__)

  def ore
    puts "俺が"
  end

  def dhh
    puts "DHH、こと"
  end

  def da
    puts "David Heinemeier Hanssonだ!"
  end
end

rails generate コマンドを叩くと TestGenerator クラスのインスタンスメソッド上から全部実行される

% rails generate test test # test という引数を仮に与えている
俺が
DHH、こと
David Heinemeier Hanssonだ!

ご覧のとおり上から実行されていることがわかる。
なのでファイルを作りたかったら、そういうメソッドを書けばよい。
ディレクトリを掘りたかったら、そういうメソッドを書けば良い。簡単

lib/generators/test/USAGE

中身は以下の通り。

Description:
    Explain the generator

Example:
    rails generate test Thing

    This will create:
        what/will/it/create

ヘルプコマンドの下の方にそのまま使われる

% rails generate test -h
Usage:
  rails generate test NAME [options]

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

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:
    Explain the generator

Example:
    rails generate test Thing

    This will create:
        what/will/it/create

lib/generators/test/templates/

ディレクトリ。現在は空。 erb テンプレートファイルを置いておく用のディレクトリ

test/lib/generators/test_generator_test.rb

require 'test_helper'
require 'generators/test/test_generator'

class TestGeneratorTest < Rails::Generators::TestCase
  tests TestGenerator
  destination Rails.root.join('tmp/generators')
  setup :prepare_destination

  # test "generator runs without errors" do
  #   assert_nothing_raised do
  #     run_generator ["arguments"]
  #   end
  # end
end

テストの雛形。rspec 使ってれば rspec で出してくれる。 ...と思いきや、rspec の場合はスケルトン作成に失敗する。
(失敗の原因を詳しく追ってない。本当は作られるはず... なのかな)

継承クラス

親クラスは若干注意が必要なので、ここでやりたいことに合わせて分岐する

Rails::Generators::NamedBase

default だとこっち。 rails generate コマンドの引数として、一つ何かを受け取ることが必須のクラス

cf. Rails::Generators::NamedBase

% rails generate controller Users xxxx のような馴染みのある generator を作りたい場合はこちらで良い。

Rails::Generators::Base

NamedBase とは異なり rails generate コマンドの引数を自由に定義できるクラス。 Rails::Generators::NamedBaseRails::Generators::Base を継承している。

こちらのほうが柔軟性があるが、特別な用途がない限りは使わなくても良いかと思います。

便利メソッド

generator なので、テンプレートからファイル生成したり、ディレクトリを掘ったりする用途が主である。
rails はすごいから、そういう簡単なものは既にメソッドが用意してある。

これらのメソッドは全て Thor::Actions に定義されているので、そっちを見たほうがいい。 というのも Rails::Generators::BaseThor::Actions を include しているから。

もっと言うと Rails::Generators::BaseThor::Group を継承して作られているので、rails の generator は大体 thor でできてると言って良い。

したがって困ったときは thor で検索すると良い

以下は便利メソッドの内、いくつかを紹介

create_file

https://github.com/erikhuda/thor/blob/067f6638f95bd000b0a92cfb45b668bca5b0efe3/lib/thor/actions/create_file.rb#L22
file を生成する

directory

https://github.com/erikhuda/thor/blob/067f6638f95bd000b0a92cfb45b668bca5b0efe3/lib/thor/actions/directory.rb#L49

directory を copy する

template

https://github.com/erikhuda/thor/blob/067f6638f95bd000b0a92cfb45b668bca5b0efe3/lib/thor/actions/file_manipulation.rb#L108

ERB テンプレートを使って、ファイルを生成

具体例

class TestGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('../templates', __FILE__)

  def create_empty_file
    create_file "aaaa.txt"
  end
end
% rails g test arg1
      create  aaaa.txt

見たことある出力やー

% rails g test arg1
   identical  Test.txt

もう一回やると、またまた見たことある出力に。
大体こんな感じでノリで generator 作れてしまいます。すごい簡単です。

まとめ

  • 継承するクラスによって、generator に渡せる引数が変わる
    • Rails::Generators::NamedBase は 1 つの引数を受け取る
    • Rails::Generators::Base は自在に定義可能
  • rails generator の内部は殆ど thor
    • 詰まったら thor で調べると良い
    • thor を一度触ったことがある人は知見を十分に活かせる
      • thor はとても素直な実装になっているから
      • thor の spec はとてもわかり易いので、読むだけで理解できる
  • hook_for は謎が多いので、使う時は若干注意
  • thor は「トール」と読むらしい

参考