Help us understand the problem. What is going on with this article?

Rails の GeneratorGenerator を使ってみる

More than 3 years have passed since last update.

はじめに

Rails でモデルを作ったりコントローラを作ったりするとき、よく rails generate model ... とか rails generate controller ...といったコマンドを使うと思いますが、今回採り上げる GeneratorGenerator は、そういう Model ジェネレータや Controller ジェネレータのようなジェネレータを作るジェネレータです1

簡単なサンプル

さて、さっそくコマンドを使ってみましょう。ここでは Hello というジェネレータを作成しようとしています。

$ bundle exec rails generate generator Hello

これで lib/generators/hello というディレクトリができ、そこに hello_generator.rb その他のファイルが生成されます。
これがジェネレータになるわけですが、もちろん rails generate コマンドは基本的にテンプレートを作成するだけですから、hello_generator.rb の中身は次のようにほぼ空っぽです。

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

空のままではつまらないので、とりあえず曲がりなりにも動いたなと思えるようになるまでコードを書き足してみましょう。

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

  def hello
    puts "You specified `#{self.name}' as name"
  end

  def goodbye
    puts 'Bye-bye'
  end

  protected

  def foo
    puts :foo
  end

  private

  def baa
    puts :baa
  end
end

単純に puts するばかりのメソッドを追加してみました。では実行してみましょう。hello ジェネレータを World という引数を伴って実行しています。

$ bundle exec rails generate hello World
You specified `World' as name
Bye-bye

文字列が出力されて終了しましたね。文字列の出力だけですから、ジェネレータとしての機能をまったく満たしていない物足りなさはありますが、とにかく動作したことは確認できたと思います。今はとりあえずこれでよしとしておきましょう。

動作についての簡単な説明

ジェネレータの動きについて、みておきましょう。動作原理ついては見たままともいえるので説明なしに推測可能かもしれませんが、解説してみます。
まずユーザが HelloGenerator に対して渡した引数 World の扱いについて着目してみます。

Rails::Generators::NamedBase を継承した HelloGenerator は、デフォルトで引数をひとつとります。プログラム内部でこの引数の名前は name として認識され、self.name などのゲッタでアクセスできるようになります。

ですので、hello メソッド内で self.name を評価することで World という出力を得ているわけです。

続いてメソッドがどのように実行されているかを見てみましょう。
これはこれこそ見たままという感じです。つまり、メソッドが記述された順に何も書かずとも順次実行されます。ただし、protected や private のメソッドはその限りではありません2

テンプレートを使ってファイルに出力

ジェネレータであるからには、やはり何かファイルに出力しておくべきでしょう。ということで、簡単ですがそんなサンプルを作ってみたいと思います。ここでは Amazon Web Services の CloudSearch をイメージして、あらたに CloudSearchGenerator を作ってみます。

$ bin/rails g generator CloudSearch

まずはテンプレートの作成です。lib/generators/cloud_search/templates/index_fields.yml.erb にこんな感じのものを書いてみましょう。

domain_name: <%= name %>
index_field:
<% attributes.each do |attr| -%>
  - index_field_name: <%= attr.name %>
    index_field_type: <%= attr.type %>
    <%- if attr.type == :string -%>
    text_options:
      default_value: ""
      source_field: ""
      return_enabled: true
      sort_enabled: true
      highlight_enabled: true
      analysis_scheme: "Japanese"
    <%- end -%>
<% end -%>

option については string タイプのフィールドのことしか書いていませんが、サンプルなのでご勘弁を。

続いてジェネレータ本体をつくります。

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

  argument :attributes, type: :array, default: [], banner: 'field:type'

  def output
    base_path = File.join("cloud_search", class_path)
    empty_directory base_path

    @path = File.join(base_path, "#{file_name}.yml")
    template "index_fields.yml.erb", @path
  end
end

これで完成です。
ではさっそく実行してみましょう。ここでは mydoc というドメインを作成し、そこに titlecontent というフィールドを設定することイメージしています。

$ bin/rails g cloud_search mydoc title:string content:string
      create  cloud_search
      create  cloud_search/mydoc.yml

ファイルが cloud_search/mydoc.yml として作成されました! 中身も期待どおりできているはずです。

動作についての簡単な説明

templates ディレクトリにあるテンプレートは template メソッドで適用できます。簡単にファイルを生成できますね。

なお、Rails::Generators::NamedBase を継承したジェネレータでは、引数 attributes は特殊な動作をします。
attributes のパースの仕様は、DB のフィールドを想定して設計されているので、CloudSearch 用にはそのままでは適用しにくいところがあります。
使う場合にはパースの仕様をカスタマイズする必要があるでしょうが、この記事はサンプルを目的にしているのでここで留めておこうと思います。

最後に

ここでは CloudSearch をイメージしたファイルの生成などをおこないましたが、ジェネレータは、そのほかにも、たとえば引数に基づいた設定ファイルの生成なども可能でしょう。
引数を渡すのはユーザだけではなく、たとえばデプロイツールにやらせることも可能かもしれず、環境ごとに設定ファイルを生成しわけたい場合にも利用できるかもしれません。
アイディア次第でさまざまシーンで活用できるのではないかな、と思います。

ジェネレータはもともと「コードを生成するコード」でありメタな存在といえますが、この論法でいくと、Generatorジェネレータは「コードを生成するコードを生成するコード」であり、メタメタです。
このメタっぷりは、魔術が飛び交う Rails の世界にふさわしい興味深いプログラムではないかと思います。
みなさんもGeneratorジェネレータでメタまみれになってみませんか?


  1. もともとは Rails のジェネレータを支えている Thor について調べながら何か書いてみようと思っていたのですが、いきなり Thor を話題にするより、まずは Rails の話から入ったほうが、書くにしても勢いをつけられそうかなと思いました。続きが書けるかは見通しはないのですけれどもね。 

  2. このように public メソッドが自然に順次実行されるのは Rails::Generators::NamedBaseThor::Group を継承しているからです。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした